Canvas实战:用fillRect、strokeRect和clearRect打造动态涂鸦板(Vue3版)
Canvas实战用fillRect、strokeRect和clearRect打造动态涂鸦板Vue3版在数字创作领域Canvas一直是前端开发者手中的魔法画笔。当Vue3的响应式特性遇上Canvas的底层绘图能力我们能创造出怎样的互动奇迹本文将带你从零构建一个功能完整的涂鸦板重点探索fillRect、strokeRect和clearRect这三个矩形绘制方法在动态交互中的巧妙应用。1. 环境搭建与基础绘制首先创建一个Vue3项目我们需要准备画布环境和基础绘制能力template div classcanvas-container canvas refcanvas mousedownstartDrawing mousemovedraw mouseupstopDrawing mouseleavestopDrawing /canvas /div /template script setup import { ref, onMounted } from vue const canvas ref(null) const isDrawing ref(false) const currentMode ref(fill) // fill/stroke/clear const ctx ref(null) onMounted(() { const canvasEl canvas.value canvasEl.width canvasEl.offsetWidth canvasEl.height canvasEl.offsetHeight ctx.value canvasEl.getContext(2d) }) /script基础绘制方法实现// 在setup()中添加 const startPos ref({ x: 0, y: 0 }) const startDrawing (e) { isDrawing.value true startPos.value { x: e.offsetX, y: e.offsetY } } const draw (e) { if (!isDrawing.value) return const currentX e.offsetX const currentY e.offsetY const width currentX - startPos.value.x const height currentY - startPos.value.y // 临时绘制预览 ctx.value.clearRect(0, 0, canvas.value.width, canvas.value.height) redrawHistory() switch (currentMode.value) { case fill: ctx.value.fillRect(startPos.value.x, startPos.value.y, width, height) break case stroke: ctx.value.strokeRect(startPos.value.x, startPos.value.y, width, height) break case clear: ctx.value.clearRect(startPos.value.x, startPos.value.y, width, height) break } } const stopDrawing () { isDrawing.value false saveToHistory() }2. 实现撤销/重做功能涂鸦板的核心体验在于可修改性我们需要实现历史记录功能const history ref([]) const historyIndex ref(-1) const saveToHistory () { // 截取当前画布状态 const imageData ctx.value.getImageData( 0, 0, canvas.value.width, canvas.value.height ) // 如果我们在历史记录中间进行了新操作需要丢弃后面的记录 history.value history.value.slice(0, historyIndex.value 1) history.value.push(imageData) historyIndex.value history.value.length - 1 } const redrawHistory () { if (historyIndex.value 0) { ctx.value.putImageData(history.value[historyIndex.value], 0, 0) } else { ctx.value.clearRect(0, 0, canvas.value.width, canvas.value.height) } } const undo () { if (historyIndex.value 0) { historyIndex.value-- redrawHistory() } } const redo () { if (historyIndex.value history.value.length - 1) { historyIndex.value redrawHistory() } }3. 高级绘制模式实现让我们的涂鸦板支持更多创意表达3.1 混合模式切换const toggleMode (mode) { currentMode.value mode // 更新绘制样式 switch(mode) { case fill: ctx.value.fillStyle #3aa8c1 break case stroke: ctx.value.strokeStyle #e74c3c ctx.value.lineWidth 5 break } }3.2 动态颜色选择添加颜色选择器组件input typecolor v-modelcurrentColor changeupdateStyle script setup const currentColor ref(#3aa8c1) const updateStyle () { ctx.value.fillStyle currentColor.value ctx.value.strokeStyle currentColor.value } /script3.3 笔触大小控制input typerange min1 max20 v-modellineWidth inputupdateLineWidth script setup const lineWidth ref(5) const updateLineWidth () { ctx.value.lineWidth lineWidth.value } /script4. 性能优化与进阶技巧4.1 双缓冲技术避免绘制时的闪烁问题const setupDoubleBuffer () { const bufferCanvas document.createElement(canvas) bufferCanvas.width canvas.value.width bufferCanvas.height canvas.value.height const bufferCtx bufferCanvas.getContext(2d) return { bufferCanvas, bufferCtx } } // 修改draw函数 const draw (e) { if (!isDrawing.value) return const { bufferCanvas, bufferCtx } setupDoubleBuffer() // 在缓冲画布上绘制 bufferCtx.drawImage(canvas.value, 0, 0) const currentX e.offsetX const currentY e.offsetY const width currentX - startPos.value.x const height currentY - startPos.value.y switch (currentMode.value) { case fill: bufferCtx.fillRect(startPos.value.x, startPos.value.y, width, height) break case stroke: bufferCtx.strokeRect(startPos.value.x, startPos.value.y, width, height) break case clear: bufferCtx.clearRect(startPos.value.x, startPos.value.y, width, height) break } // 一次性绘制到主画布 ctx.value.drawImage(bufferCanvas, 0, 0) }4.2 移动端适配添加触摸事件支持canvas refcanvas touchstarthandleTouchStart touchmovehandleTouchMove touchendhandleTouchEnd /canvas script setup const handleTouchStart (e) { e.preventDefault() const touch e.touches[0] const rect canvas.value.getBoundingClientRect() startDrawing({ offsetX: touch.clientX - rect.left, offsetY: touch.clientY - rect.top }) } const handleTouchMove (e) { e.preventDefault() const touch e.touches[0] const rect canvas.value.getBoundingClientRect() draw({ offsetX: touch.clientX - rect.left, offsetY: touch.clientY - rect.top }) } const handleTouchEnd (e) { e.preventDefault() stopDrawing() } /script4.3 导出功能实现const exportImage (type png) { const dataURL canvas.value.toDataURL(image/${type}) const link document.createElement(a) link.download drawing.${type} link.href dataURL link.click() }5. 创意扩展思路5.1 图案填充效果const createPattern () { const patternCanvas document.createElement(canvas) patternCanvas.width 20 patternCanvas.height 20 const patternCtx patternCanvas.getContext(2d) // 绘制简单图案 patternCtx.fillStyle #333 patternCtx.fillRect(0, 0, 10, 10) patternCtx.fillRect(10, 10, 10, 10) return ctx.value.createPattern(patternCanvas, repeat) } const drawWithPattern () { ctx.value.fillStyle createPattern() ctx.value.fillRect(50, 50, 200, 200) }5.2 实时协作功能使用WebSocket实现多人协作涂鸦const setupWebSocket () { const ws new WebSocket(wss://your-websocket-server) ws.onmessage (event) { const data JSON.parse(event.data) if (data.type draw) { drawRemote(data) } } const drawRemote (data) { ctx.value.save() ctx.value.globalAlpha 0.5 ctx.value[${data.mode}Rect](data.x, data.y, data.width, data.height) ctx.value.restore() } return ws }5.3 动画效果集成结合requestAnimationFrame实现简单动画const animateRect () { let x 0 const animate () { ctx.value.clearRect(0, 0, canvas.value.width, canvas.value.height) ctx.value.fillRect(x, 100, 50, 50) x (x 2) % canvas.value.width requestAnimationFrame(animate) } animate() }