绑定板应用

Canvas绘图板应用开发指南,涵盖绑笔工具、颜色选择、撤销重做、图层管理等功能实现。Canvas绘图板是一个经典的应用场景,本节将介绍如何开发一个功能完善的绘图板应用。

基础绘图功能

绑笔工具

class BrushTool {
  constructor(canvas) {
    this.canvas = canvas
    this.ctx = canvas.getContext('2d')
    
    this.isDrawing = false
    this.lastX = 0
    this.lastY = 0
    
    this.options = {
      color: '#000000',
      size: 5,
      opacity: 1,
      lineCap: 'round',
      lineJoin: 'round'
    }
    
    this.bindEvents()
  }
  
  bindEvents() {
    this.canvas.addEventListener('mousedown', this.onMouseDown.bind(this))
    this.canvas.addEventListener('mousemove', this.onMouseMove.bind(this))
    this.canvas.addEventListener('mouseup', this.onMouseUp.bind(this))
    this.canvas.addEventListener('mouseleave', this.onMouseUp.bind(this))
    
    this.canvas.addEventListener('touchstart', this.onTouchStart.bind(this))
    this.canvas.addEventListener('touchmove', this.onTouchMove.bind(this))
    this.canvas.addEventListener('touchend', this.onTouchEnd.bind(this))
  }
  
  getPos(e) {
    const rect = this.canvas.getBoundingClientRect()
    return {
      x: e.clientX - rect.left,
      y: e.clientY - rect.top
    }
  }
  
  getTouchPos(e) {
    const rect = this.canvas.getBoundingClientRect()
    const touch = e.touches[0]
    return {
      x: touch.clientX - rect.left,
      y: touch.clientY - rect.top
    }
  }
  
  onMouseDown(e) {
    this.isDrawing = true
    const pos = this.getPos(e)
    this.lastX = pos.x
    this.lastY = pos.y
    
    this.ctx.beginPath()
    this.ctx.arc(pos.x, pos.y, this.options.size / 2, 0, Math.PI * 2)
    this.ctx.fillStyle = this.options.color
    this.ctx.globalAlpha = this.options.opacity
    this.ctx.fill()
    this.ctx.globalAlpha = 1
  }
  
  onMouseMove(e) {
    if (!this.isDrawing) return
    
    const pos = this.getPos(e)
    
    this.ctx.beginPath()
    this.ctx.moveTo(this.lastX, this.lastY)
    this.ctx.lineTo(pos.x, pos.y)
    this.ctx.strokeStyle = this.options.color
    this.ctx.lineWidth = this.options.size
    this.ctx.lineCap = this.options.lineCap
    this.ctx.lineJoin = this.options.lineJoin
    this.ctx.globalAlpha = this.options.opacity
    this.ctx.stroke()
    this.ctx.globalAlpha = 1
    
    this.lastX = pos.x
    this.lastY = pos.y
  }
  
  onMouseUp() {
    this.isDrawing = false
  }
  
  onTouchStart(e) {
    e.preventDefault()
    this.isDrawing = true
    const pos = this.getTouchPos(e)
    this.lastX = pos.x
    this.lastY = pos.y
  }
  
  onTouchMove(e) {
    e.preventDefault()
    if (!this.isDrawing) return
    
    const pos = this.getTouchPos(e)
    
    this.ctx.beginPath()
    this.ctx.moveTo(this.lastX, this.lastY)
    this.ctx.lineTo(pos.x, pos.y)
    this.ctx.strokeStyle = this.options.color
    this.ctx.lineWidth = this.options.size
    this.ctx.lineCap = this.options.lineCap
    this.ctx.lineJoin = this.options.lineJoin
    this.ctx.globalAlpha = this.options.opacity
    this.ctx.stroke()
    this.ctx.globalAlpha = 1
    
    this.lastX = pos.x
    this.lastY = pos.y
  }
  
  onTouchEnd() {
    this.isDrawing = false
  }
  
  setColor(color) {
    this.options.color = color
  }
  
  setSize(size) {
    this.options.size = size
  }
  
  setOpacity(opacity) {
    this.options.opacity = opacity
  }
}

橡皮擦工具

class EraserTool {
  constructor(canvas) {
    this.canvas = canvas
    this.ctx = canvas.getContext('2d')
    
    this.isDrawing = false
    this.lastX = 0
    this.lastY = 0
    this.size = 20
    
    this.bindEvents()
  }
  
  bindEvents() {
    this.canvas.addEventListener('mousedown', this.onMouseDown.bind(this))
    this.canvas.addEventListener('mousemove', this.onMouseMove.bind(this))
    this.canvas.addEventListener('mouseup', this.onMouseUp.bind(this))
    this.canvas.addEventListener('mouseleave', this.onMouseUp.bind(this))
  }
  
  onMouseDown(e) {
    this.isDrawing = true
    const pos = this.getPos(e)
    this.lastX = pos.x
    this.lastY = pos.y
    
    this.ctx.save()
    this.ctx.globalCompositeOperation = 'destination-out'
    this.ctx.beginPath()
    this.ctx.arc(pos.x, pos.y, this.size / 2, 0, Math.PI * 2)
    this.ctx.fill()
    this.ctx.restore()
  }
  
  onMouseMove(e) {
    if (!this.isDrawing) return
    
    const pos = this.getPos(e)
    
    this.ctx.save()
    this.ctx.globalCompositeOperation = 'destination-out'
    this.ctx.beginPath()
    this.ctx.moveTo(this.lastX, this.lastY)
    this.ctx.lineTo(pos.x, pos.y)
    this.ctx.lineWidth = this.size
    this.ctx.lineCap = 'round'
    this.ctx.stroke()
    this.ctx.restore()
    
    this.lastX = pos.x
    this.lastY = pos.y
  }
  
  onMouseUp() {
    this.isDrawing = false
  }
  
  getPos(e) {
    const rect = this.canvas.getBoundingClientRect()
    return {
      x: e.clientX - rect.left,
      y: e.clientY - rect.top
    }
  }
}

绘图板示例

简单绘图板

撤销与重做

历史记录管理

class HistoryManager {
  constructor(maxHistory = 50) {
    this.history = []
    this.currentIndex = -1
    this.maxHistory = maxHistory
  }
  
  push(state) {
    this.history = this.history.slice(0, this.currentIndex + 1)
    this.history.push(state)
    
    if (this.history.length > this.maxHistory) {
      this.history.shift()
    } else {
      this.currentIndex++
    }
  }
  
  undo() {
    if (this.currentIndex > 0) {
      this.currentIndex--
      return this.history[this.currentIndex]
    }
    return null
  }
  
  redo() {
    if (this.currentIndex < this.history.length - 1) {
      this.currentIndex++
      return this.history[this.currentIndex]
    }
    return null
  }
  
  canUndo() {
    return this.currentIndex > 0
  }
  
  canRedo() {
    return this.currentIndex < this.history.length - 1
  }
  
  clear() {
    this.history = []
    this.currentIndex = -1
  }
}

绘图板集成

class DrawingBoard {
  constructor(canvasId, options = {}) {
    this.canvas = document.getElementById(canvasId)
    this.ctx = this.canvas.getContext('2d')
    
    this.options = {
      backgroundColor: '#ffffff',
      ...options
    }
    
    this.history = new HistoryManager()
    this.currentTool = null
    this.tools = new Map()
    
    this.init()
  }
  
  init() {
    this.ctx.fillStyle = this.options.backgroundColor
    this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height)
    
    this.saveState()
    
    this.addTool('brush', new BrushTool(this.canvas))
    this.addTool('eraser', new EraserTool(this.canvas))
    
    this.setTool('brush')
    
    this.bindKeyboard()
  }
  
  addTool(name, tool) {
    this.tools.set(name, tool)
  }
  
  setTool(name) {
    this.currentTool = this.tools.get(name)
  }
  
  saveState() {
    const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)
    this.history.push(imageData)
  }
  
  undo() {
    const state = this.history.undo()
    if (state) {
      this.ctx.putImageData(state, 0, 0)
    }
  }
  
  redo() {
    const state = this.history.redo()
    if (state) {
      this.ctx.putImageData(state, 0, 0)
    }
  }
  
  clear() {
    this.saveState()
    this.ctx.fillStyle = this.options.backgroundColor
    this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height)
  }
  
  bindKeyboard() {
    document.addEventListener('keydown', (e) => {
      if (e.ctrlKey || e.metaKey) {
        if (e.key === 'z') {
          e.preventDefault()
          if (e.shiftKey) {
            this.redo()
          } else {
            this.undo()
          }
        }
      }
    })
  }
  
  exportImage(format = 'image/png') {
    return this.canvas.toDataURL(format)
  }
  
  download(filename = 'drawing.png') {
    const link = document.createElement('a')
    link.download = filename
    link.href = this.exportImage()
    link.click()
  }
}

图层系统

class Layer {
  constructor(width, height, name = 'Layer') {
    this.canvas = document.createElement('canvas')
    this.canvas.width = width
    this.canvas.height = height
    this.ctx = this.canvas.getContext('2d')
    
    this.name = name
    this.visible = true
    this.opacity = 1
    this.blendMode = 'source-over'
    this.locked = false
  }
  
  clear() {
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
  }
  
  getImageData() {
    return this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)
  }
  
  putImageData(imageData) {
    this.ctx.putImageData(imageData, 0, 0)
  }
}

class LayerManager {
  constructor(width, height) {
    this.width = width
    this.height = height
    this.layers = []
    this.activeLayerIndex = -1
  }
  
  addLayer(name) {
    const layer = new Layer(this.width, this.height, name || `Layer ${this.layers.length + 1}`)
    this.layers.push(layer)
    this.activeLayerIndex = this.layers.length - 1
    return layer
  }
  
  removeLayer(index) {
    if (index >= 0 && index < this.layers.length) {
      this.layers.splice(index, 1)
      if (this.activeLayerIndex >= this.layers.length) {
        this.activeLayerIndex = this.layers.length - 1
      }
    }
  }
  
  getActiveLayer() {
    return this.layers[this.activeLayerIndex]
  }
  
  setActiveLayer(index) {
    if (index >= 0 && index < this.layers.length) {
      this.activeLayerIndex = index
    }
  }
  
  moveLayer(fromIndex, toIndex) {
    if (fromIndex >= 0 && fromIndex < this.layers.length &&
        toIndex >= 0 && toIndex < this.layers.length) {
      const layer = this.layers.splice(fromIndex, 1)[0]
      this.layers.splice(toIndex, 0, layer)
    }
  }
  
  mergeDown(index) {
    if (index > 0 && index < this.layers.length) {
      const upperLayer = this.layers[index]
      const lowerLayer = this.layers[index - 1]
      
      lowerLayer.ctx.globalAlpha = upperLayer.opacity
      lowerLayer.ctx.globalCompositeOperation = upperLayer.blendMode
      lowerLayer.ctx.drawImage(upperLayer.canvas, 0, 0)
      lowerLayer.ctx.globalAlpha = 1
      lowerLayer.ctx.globalCompositeOperation = 'source-over'
      
      this.layers.splice(index, 1)
    }
  }
  
  flatten() {
    const result = new Layer(this.width, this.height, 'Merged')
    
    this.layers.forEach(layer => {
      if (layer.visible) {
        result.ctx.globalAlpha = layer.opacity
        result.ctx.globalCompositeOperation = layer.blendMode
        result.ctx.drawImage(layer.canvas, 0, 0)
      }
    })
    
    result.ctx.globalAlpha = 1
    result.ctx.globalCompositeOperation = 'source-over'
    
    return result
  }
  
  render(targetCtx) {
    targetCtx.clearRect(0, 0, this.width, this.height)
    
    this.layers.forEach(layer => {
      if (layer.visible) {
        targetCtx.globalAlpha = layer.opacity
        targetCtx.globalCompositeOperation = layer.blendMode
        targetCtx.drawImage(layer.canvas, 0, 0)
      }
    })
    
    targetCtx.globalAlpha = 1
    targetCtx.globalCompositeOperation = 'source-over'
  }
}

形状工具

矩形工具

class RectangleTool {
  constructor(canvas, layerManager) {
    this.canvas = canvas
    this.layerManager = layerManager
    
    this.isDrawing = false
    this.startX = 0
    this.startY = 0
    
    this.options = {
      fillColor: '#3498db',
      strokeColor: '#2980b9',
      strokeWidth: 2,
      fill: true,
      stroke: true,
      cornerRadius: 0
    }
    
    this.tempCanvas = document.createElement('canvas')
    this.tempCanvas.width = canvas.width
    this.tempCanvas.height = canvas.height
    this.tempCtx = this.tempCanvas.getContext('2d')
    
    this.bindEvents()
  }
  
  bindEvents() {
    this.canvas.addEventListener('mousedown', this.onMouseDown.bind(this))
    this.canvas.addEventListener('mousemove', this.onMouseMove.bind(this))
    this.canvas.addEventListener('mouseup', this.onMouseUp.bind(this))
  }
  
  onMouseDown(e) {
    this.isDrawing = true
    const pos = this.getPos(e)
    this.startX = pos.x
    this.startY = pos.y
    
    const layer = this.layerManager.getActiveLayer()
    if (layer) {
      this.tempCtx.clearRect(0, 0, this.tempCanvas.width, this.tempCanvas.height)
      this.tempCtx.drawImage(layer.canvas, 0, 0)
    }
  }
  
  onMouseMove(e) {
    if (!this.isDrawing) return
    
    const pos = this.getPos(e)
    const layer = this.layerManager.getActiveLayer()
    
    if (layer) {
      layer.clear()
      layer.ctx.drawImage(this.tempCanvas, 0, 0)
      
      const x = Math.min(this.startX, pos.x)
      const y = Math.min(this.startY, pos.y)
      const width = Math.abs(pos.x - this.startX)
      const height = Math.abs(pos.y - this.startY)
      
      this.drawRectangle(layer.ctx, x, y, width, height)
    }
  }
  
  onMouseUp(e) {
    this.isDrawing = false
  }
  
  drawRectangle(ctx, x, y, width, height) {
    ctx.beginPath()
    
    if (this.options.cornerRadius > 0) {
      const r = Math.min(this.options.cornerRadius, width / 2, height / 2)
      ctx.moveTo(x + r, y)
      ctx.lineTo(x + width - r, y)
      ctx.quadraticCurveTo(x + width, y, x + width, y + r)
      ctx.lineTo(x + width, y + height - r)
      ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height)
      ctx.lineTo(x + r, y + height)
      ctx.quadraticCurveTo(x, y + height, x, y + height - r)
      ctx.lineTo(x, y + r)
      ctx.quadraticCurveTo(x, y, x + r, y)
    } else {
      ctx.rect(x, y, width, height)
    }
    
    if (this.options.fill) {
      ctx.fillStyle = this.options.fillColor
      ctx.fill()
    }
    
    if (this.options.stroke) {
      ctx.strokeStyle = this.options.strokeColor
      ctx.lineWidth = this.options.strokeWidth
      ctx.stroke()
    }
  }
  
  getPos(e) {
    const rect = this.canvas.getBoundingClientRect()
    return {
      x: e.clientX - rect.left,
      y: e.clientY - rect.top
    }
  }
}

圆形工具

class CircleTool {
  constructor(canvas, layerManager) {
    this.canvas = canvas
    this.layerManager = layerManager
    
    this.isDrawing = false
    this.startX = 0
    this.startY = 0
    
    this.options = {
      fillColor: '#e74c3c',
      strokeColor: '#c0392b',
      strokeWidth: 2,
      fill: true,
      stroke: true
    }
    
    this.tempCanvas = document.createElement('canvas')
    this.tempCanvas.width = canvas.width
    this.tempCanvas.height = canvas.height
    this.tempCtx = this.tempCanvas.getContext('2d')
    
    this.bindEvents()
  }
  
  bindEvents() {
    this.canvas.addEventListener('mousedown', this.onMouseDown.bind(this))
    this.canvas.addEventListener('mousemove', this.onMouseMove.bind(this))
    this.canvas.addEventListener('mouseup', this.onMouseUp.bind(this))
  }
  
  onMouseDown(e) {
    this.isDrawing = true
    const pos = this.getPos(e)
    this.startX = pos.x
    this.startY = pos.y
    
    const layer = this.layerManager.getActiveLayer()
    if (layer) {
      this.tempCtx.clearRect(0, 0, this.tempCanvas.width, this.tempCanvas.height)
      this.tempCtx.drawImage(layer.canvas, 0, 0)
    }
  }
  
  onMouseMove(e) {
    if (!this.isDrawing) return
    
    const pos = this.getPos(e)
    const layer = this.layerManager.getActiveLayer()
    
    if (layer) {
      layer.clear()
      layer.ctx.drawImage(this.tempCanvas, 0, 0)
      
      const dx = pos.x - this.startX
      const dy = pos.y - this.startY
      const radius = Math.sqrt(dx * dx + dy * dy)
      
      layer.ctx.beginPath()
      layer.ctx.arc(this.startX, this.startY, radius, 0, Math.PI * 2)
      
      if (this.options.fill) {
        layer.ctx.fillStyle = this.options.fillColor
        layer.ctx.fill()
      }
      
      if (this.options.stroke) {
        layer.ctx.strokeStyle = this.options.strokeColor
        layer.ctx.lineWidth = this.options.strokeWidth
        layer.ctx.stroke()
      }
    }
  }
  
  onMouseUp() {
    this.isDrawing = false
  }
  
  getPos(e) {
    const rect = this.canvas.getBoundingClientRect()
    return {
      x: e.clientX - rect.left,
      y: e.clientY - rect.top
    }
  }
}

文本工具

class TextTool {
  constructor(canvas, layerManager) {
    this.canvas = canvas
    this.layerManager = layerManager
    
    this.options = {
      fontFamily: 'Arial',
      fontSize: 24,
      fontWeight: 'normal',
      fontStyle: 'normal',
      color: '#000000',
      align: 'left',
      baseline: 'top'
    }
    
    this.input = null
    this.bindEvents()
  }
  
  bindEvents() {
    this.canvas.addEventListener('click', this.onClick.bind(this))
  }
  
  onClick(e) {
    if (this.input) {
      this.commitText()
      return
    }
    
    const pos = this.getPos(e)
    this.createInput(pos.x, pos.y)
  }
  
  createInput(x, y) {
    this.input = document.createElement('input')
    this.input.type = 'text'
    this.input.style.position = 'absolute'
    this.input.style.left = `${x}px`
    this.input.style.top = `${y}px`
    this.input.style.fontFamily = this.options.fontFamily
    this.input.style.fontSize = `${this.options.fontSize}px`
    this.input.style.fontWeight = this.options.fontWeight
    this.input.style.fontStyle = this.options.fontStyle
    this.input.style.color = this.options.color
    this.input.style.background = 'transparent'
    this.input.style.border = '1px dashed #3498db'
    this.input.style.outline = 'none'
    this.input.style.padding = '2px'
    
    this.canvas.parentElement.appendChild(this.input)
    this.input.focus()
    
    this.input.addEventListener('blur', () => this.commitText())
    this.input.addEventListener('keydown', (e) => {
      if (e.key === 'Enter') {
        this.commitText()
      }
    })
  }
  
  commitText() {
    if (!this.input || !this.input.value) {
      this.removeInput()
      return
    }
    
    const layer = this.layerManager.getActiveLayer()
    if (layer) {
      const rect = this.input.getBoundingClientRect()
      const canvasRect = this.canvas.getBoundingClientRect()
      
      const x = rect.left - canvasRect.left
      const y = rect.top - canvasRect.top
      
      layer.ctx.font = `${this.options.fontStyle} ${this.options.fontWeight} ${this.options.fontSize}px ${this.options.fontFamily}`
      layer.ctx.fillStyle = this.options.color
      layer.ctx.textAlign = this.options.align
      layer.ctx.textBaseline = this.options.baseline
      layer.ctx.fillText(this.input.value, x, y)
    }
    
    this.removeInput()
  }
  
  removeInput() {
    if (this.input) {
      this.input.remove()
      this.input = null
    }
  }
  
  getPos(e) {
    const rect = this.canvas.getBoundingClientRect()
    return {
      x: e.clientX - rect.left,
      y: e.clientY - rect.top
    }
  }
}

选区工具

class SelectionTool {
  constructor(canvas) {
    this.canvas = canvas
    this.ctx = canvas.getContext('2d')
    
    this.selection = null
    this.isSelecting = false
    this.startX = 0
    this.startY = 0
    
    this.marchingAntsOffset = 0
    
    this.bindEvents()
    this.startMarchingAnts()
  }
  
  bindEvents() {
    this.canvas.addEventListener('mousedown', this.onMouseDown.bind(this))
    this.canvas.addEventListener('mousemove', this.onMouseMove.bind(this))
    this.canvas.addEventListener('mouseup', this.onMouseUp.bind(this))
  }
  
  onMouseDown(e) {
    const pos = this.getPos(e)
    
    if (this.selection && this.isInsideSelection(pos.x, pos.y)) {
      this.startMove(pos.x, pos.y)
    } else {
      this.isSelecting = true
      this.startX = pos.x
      this.startY = pos.y
      this.selection = null
    }
  }
  
  onMouseMove(e) {
    const pos = this.getPos(e)
    
    if (this.isSelecting) {
      this.selection = {
        x: Math.min(this.startX, pos.x),
        y: Math.min(this.startY, pos.y),
        width: Math.abs(pos.x - this.startX),
        height: Math.abs(pos.y - this.startY)
      }
    }
  }
  
  onMouseUp() {
    this.isSelecting = false
  }
  
  isInsideSelection(x, y) {
    if (!this.selection) return false
    return x >= this.selection.x &&
           x <= this.selection.x + this.selection.width &&
           y >= this.selection.y &&
           y <= this.selection.y + this.selection.height
  }
  
  startMarchingAnts() {
    const animate = () => {
      this.marchingAntsOffset = (this.marchingAntsOffset + 1) % 16
      requestAnimationFrame(animate)
    }
    animate()
  }
  
  drawMarchingAnts() {
    if (!this.selection) return
    
    this.ctx.save()
    this.ctx.strokeStyle = '#000'
    this.ctx.lineWidth = 1
    this.ctx.setLineDash([4, 4])
    this.ctx.lineDashOffset = -this.marchingAntsOffset
    
    this.ctx.strokeRect(
      this.selection.x,
      this.selection.y,
      this.selection.width,
      this.selection.height
    )
    
    this.ctx.strokeStyle = '#fff'
    this.ctx.lineDashOffset = -this.marchingAntsOffset + 4
    this.ctx.strokeRect(
      this.selection.x,
      this.selection.y,
      this.selection.width,
      this.selection.height
    )
    
    this.ctx.restore()
  }
  
  clearSelection() {
    this.selection = null
  }
  
  getPos(e) {
    const rect = this.canvas.getBoundingClientRect()
    return {
      x: e.clientX - rect.left,
      y: e.clientY - rect.top
    }
  }
}

图像导入导出

class ImageIO {
  constructor(drawingBoard) {
    this.drawingBoard = drawingBoard
  }
  
  loadImage(file) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader()
      
      reader.onload = (e) => {
        const img = new Image()
        img.onload = () => {
          this.drawingBoard.ctx.drawImage(img, 0, 0)
          this.drawingBoard.saveState()
          resolve(img)
        }
        img.onerror = reject
        img.src = e.target.result
      }
      
      reader.onerror = reject
      reader.readAsDataURL(file)
    })
  }
  
  loadImageFromURL(url) {
    return new Promise((resolve, reject) => {
      const img = new Image()
      img.crossOrigin = 'anonymous'
      img.onload = () => {
        this.drawingBoard.ctx.drawImage(img, 0, 0)
        this.drawingBoard.saveState()
        resolve(img)
      }
      img.onerror = reject
      img.src = url
    })
  }
  
  exportAsPNG() {
    return this.drawingBoard.canvas.toDataURL('image/png')
  }
  
  exportAsJPEG(quality = 0.9) {
    return this.drawingBoard.canvas.toDataURL('image/jpeg', quality)
  }
  
  download(filename = 'drawing.png', format = 'image/png') {
    const link = document.createElement('a')
    link.download = filename
    link.href = this.drawingBoard.canvas.toDataURL(format)
    link.click()
  }
}

完整绘图板类

class AdvancedDrawingBoard {
  constructor(canvasId, options = {}) {
    this.canvas = document.getElementById(canvasId)
    this.ctx = this.canvas.getContext('2d')
    
    this.options = {
      width: 800,
      height: 600,
      backgroundColor: '#ffffff',
      ...options
    }
    
    this.canvas.width = this.options.width
    this.canvas.height = this.options.height
    
    this.layerManager = new LayerManager(this.options.width, this.options.height)
    this.layerManager.addLayer('Background')
    
    this.history = new HistoryManager()
    this.tools = new Map()
    this.currentTool = null
    
    this.init()
  }
  
  init() {
    this.addTool('brush', new BrushTool(this.canvas))
    this.addTool('eraser', new EraserTool(this.canvas))
    this.addTool('rectangle', new RectangleTool(this.canvas, this.layerManager))
    this.addTool('circle', new CircleTool(this.canvas, this.layerManager))
    this.addTool('text', new TextTool(this.canvas, this.layerManager))
    
    this.setTool('brush')
    this.bindKeyboard()
    
    this.ctx.fillStyle = this.options.backgroundColor
    this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height)
    this.saveState()
  }
  
  addTool(name, tool) {
    this.tools.set(name, tool)
  }
  
  setTool(name) {
    this.currentTool = this.tools.get(name)
  }
  
  saveState() {
    const layer = this.layerManager.getActiveLayer()
    if (layer) {
      this.history.push(layer.getImageData())
    }
  }
  
  undo() {
    const state = this.history.undo()
    if (state) {
      const layer = this.layerManager.getActiveLayer()
      if (layer) {
        layer.putImageData(state)
      }
    }
  }
  
  redo() {
    const state = this.history.redo()
    if (state) {
      const layer = this.layerManager.getActiveLayer()
      if (layer) {
        layer.putImageData(state)
      }
    }
  }
  
  clear() {
    const layer = this.layerManager.getActiveLayer()
    if (layer) {
      this.saveState()
      layer.clear()
    }
  }
  
  bindKeyboard() {
    document.addEventListener('keydown', (e) => {
      if (e.ctrlKey || e.metaKey) {
        switch (e.key.toLowerCase()) {
          case 'z':
            e.preventDefault()
            e.shiftKey ? this.redo() : this.undo()
            break
          case 's':
            e.preventDefault()
            this.download()
            break
        }
      }
    })
  }
  
  download(filename = 'drawing.png') {
    const mergedLayer = this.layerManager.flatten()
    const link = document.createElement('a')
    link.download = filename
    link.href = mergedLayer.canvas.toDataURL('image/png')
    link.click()
  }
}

小结

绘图板应用开发要点:

  1. 工具系统:设计可扩展的工具接口
  2. 历史管理:实现撤销重做功能
  3. 图层系统:支持多图层编辑
  4. 形状工具:预览和绘制几何形状
  5. 文本支持:添加文本输入功能
  6. 选区功能:实现区域选择和移动
  7. 导入导出:支持图片加载和保存

通过组合这些功能,可以开发出功能完善的绘图板应用。