Canvas 绘画艺术:用 6 种特效打造手绘魔法板

0 阅读6分钟

在前端创意绘画领域,Canvas 凭借像素级操控与高性能渲染,成为实现自由绘画、特效笔触的最佳方案。我在半年前开发了一款多功能 Canvas 绘画画板,支持自由绘制、荧光、彩虹、喷绘、蜡笔、气泡等 6 种特效,还具备橡皮擦、文字输入、无限拖拽、选区操作、图层管理、撤销重做与图片导出等完整能力。

最近我发现有人把核心绘画特效的设计思路与代码完整整理出来,这篇文章将带你从零实现一套可商用的 Canvas 特效绘画系统,全文包含原理讲解、完整代码、渲染效果,可直接用于项目开发。

Canvas大神 GitHub:github.com/LHRUN/paint…欢迎 Star 支持~


一、画板核心功能总览

这款 Canvas 绘画板具备专业绘图工具的完整能力:

  • 自由绘制:支持颜色切换、根据手绘速度自动调整线条粗细
  • 6 种笔触特效:基础单色、荧光、多色渐变、喷绘、蜡笔、气泡
  • 橡皮擦:跟随鼠标线性擦除,支持擦除力度调节
  • 文字绘制:双击画板添加文字
  • 无限拖拽:按住空格拖拽画布,无边界创作
  • 选区模式:框选元素,支持移动、缩放、删除
  • 图层系统:支持添加、删除、排序图层
  • 撤销 / 重做 / 清空 / 保存:完整工程化能力

本文重点讲解6 种绘画特效的实现原理与代码,从最简单的单色笔触,到视觉效果拉满的荧光、蜡笔、气泡,全部拆解。


二、基础准备:核心架构设计

在实现特效前,先统一基础架构:

  1. 鼠标移动时记录坐标点
  2. 根据移动速度计算动态线宽
  3. 使用贝塞尔曲线平滑线条
  4. 不同特效通过渲染函数分发
  5. 统一保存坐标、线宽、颜色、配置

基础数据结构:

typescript

运行

interface MousePosition {
  x: number
  y: number
}

interface Bubble {
  radius: number
  opacity: number
}

class FreeLine {
  // 坐标集合
  positions: MousePosition[] = []
  // 线宽集合
  lineWidths: number[] = []
  // 气泡数据
  bubbles?: Bubble[]
  // 颜色
  colors: string[] = []
  // 最小/最大宽度
  minWidth = 2
  maxWidth = 20
  // 速度相关
  minSpeed = 0
  maxSpeed = 30
  lastLineWidth = this.maxWidth
  lastMoveTime = Date.now()
  // 笔触风格
  style: FreeDrawStyle
}

enum FreeDrawStyle {
  Basic = 'BASIC',
  Fluorescent = 'FLUOR',
  MultiColor = 'MULTI',
  Spray = 'SPRAY',
  Crayon = 'CRAYON',
  Bubble = 'BUBBLE'
}

三、基础单色笔触:速度感应 + 贝塞尔平滑

实现要点

  1. 速度感应线宽:鼠标移动快 → 线条变细;移动慢 → 线条变粗
  2. 贝塞尔曲线平滑:避免直线连接生硬,提升手绘质感

1. 记录坐标并计算速度与线宽

typescript

运行

addPosition(position: MousePosition) {
  this.positions.push(position)
  if (this.positions.length > 1) {
    // 计算鼠标速度
    const mouseSpeed = this._computedSpeed(
      this.positions[this.positions.length - 2],
      this.positions[this.positions.length - 1]
    )
    // 计算当前线宽
    const lineWidth = this._computedLineWidth(mouseSpeed)
    this.lineWidths.push(lineWidth)
  }
}

// 计算速度
_computedSpeed(start: MousePosition, end: MousePosition) {
  const dx = end.x - start.x
  const dy = end.y - start.y
  const dist = Math.sqrt(dx * dx + dy * dy)
  const time = Date.now() - this.lastMoveTime
  this.lastMoveTime = Date.now()
  return time === 0 ? 0 : dist / time
}

// 根据速度计算线宽
_computedLineWidth(speed: number) {
  let lineWidth = 0
  if (speed >= this.maxSpeed) {
    lineWidth = this.minWidth
  } else if (speed <= this.minSpeed) {
    lineWidth = this.maxWidth
  } else {
    lineWidth = this.maxWidth - (speed / this.maxSpeed) * this.maxWidth
  }
  // 平滑过渡
  lineWidth = lineWidth * (1 / 3) + this.lastLineWidth * (2 / 3)
  this.lastLineWidth = lineWidth
  return lineWidth
}

2. 贝塞尔曲线绘制

typescript

运行

function _drawBasic(instance: FreeLine, i: number, context: CanvasRenderingContext2D) {
  const { positions, lineWidths } = instance
  const p1 = positions[i - 1]
  const p2 = positions[i]

  context.beginPath()
  if (i === 1) {
    context.moveTo(p1.x, p1.y)
    context.lineTo(p2.x, p2.y)
  } else {
    const p0 = positions[i - 2]
    const m0 = { x: (p0.x + p1.x) / 2, y: (p0.y + p1.y) / 2 }
    const m1 = { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 }
    context.moveTo(m0.x, m0.y)
    context.quadraticCurveTo(p1.x, p1.y, m1.x, m1.y)
  }
  context.lineWidth = lineWidths[i] || this.maxWidth
  context.stroke()
}

效果:线条跟随手绘速度自然粗细变化,边缘流畅无锯齿。


四、荧光特效:阴影发光 = 高级感

荧光效果只需要在基础笔触上添加阴影颜色 + 阴影模糊

typescript

运行

case FreeDrawStyle.Fluorescent:
  context.strokeStyle = instance.colors[0]
  context.shadowColor = instance.colors[0]
  break

// 绘制时
_drawBasic(instance, i, context, () => {
  context.shadowBlur = instance.lineWidths[i]
})

效果:线条自带霓虹发光,适合夜间画板、签名、高光标注。


五、多色渐变笔触:createPattern 实现彩虹笔

使用 createPattern 创建分段色板,实现一笔多色。

typescript

运行

// 创建多色图案
function getMultiColorPattern(colors: string[]) {
  const canvas = document.createElement('canvas')
  const ctx = canvas.getContext('2d')!
  const w = 5
  canvas.width = w * colors.length
  canvas.height = 20

  colors.forEach((c, i) => {
    ctx.fillStyle = c
    ctx.fillRect(i * w, 0, w, 20)
  })
  return ctx.createPattern(canvas, 'repeat')!
}

// 渲染
case FreeDrawStyle.MultiColor:
  context.strokeStyle = getMultiColorPattern(instance.colors)
  break

效果:一笔画出彩虹渐变,适合创意绘画、儿童画板。


六、喷绘特效:随机点阵 = 喷枪质感

喷绘核心:在鼠标路径上随机生成小圆点,提前缓存 5 组随机数据避免内存暴涨。

typescript

运行

function _drawSpray(instance: FreeLine, i: number, context: CanvasRenderingContext2D) {
  const { x, y } = instance.positions[i]
  const w = instance.lineWidths[i] || 10

  // 预先生成5组随机角度、半径、透明度
  sprayPoint[i % 5].forEach(p => {
    const px = x + p.radius * Math.cos(p.angle)
    const py = y + p.radius * Math.sin(p.angle)
    // 限制在笔触范围内
    if (Math.abs(px - x) > w * 2 || Math.abs(py - y) > w * 2) return
    context.globalAlpha = p.alpha
    context.fillRect(px, py, 2, 2)
  })
}

效果:模拟喷枪喷散效果,适合素描、上色、涂鸦。


七、蜡笔特效:纹理叠加 = 真实蜡笔质感

蜡笔效果 = 纯色填充 + 半透明蜡笔纹理图案叠加。

typescript

运行

function getCrayonPattern(color: string, texture: HTMLImageElement) {
  const canvas = document.createElement('canvas')
  const ctx = canvas.getContext('2d')!
  canvas.width = 100
  canvas.height = 100

  ctx.fillStyle = color
  ctx.fillRect(0, 0, 100, 100)
  ctx.drawImage(texture, 0, 0, 100, 100)
  return ctx.createPattern(canvas, 'repeat')!
}

// 渲染
case FreeDrawStyle.Crayon:
  context.strokeStyle = getCrayonPattern(instance.colors[0], texture)
  break

效果:线条带有纸质蜡笔纹理,质感接近真实手绘蜡笔。


八、气泡特效:随机大小 + 透明度 = 梦幻感

在鼠标轨迹上生成随机半径、随机透明度的圆形气泡。

typescript

运行

// 记录气泡数据
addPosition(pos: MousePosition) {
  this.positions.push(pos)
  if (this.style === FreeDrawStyle.Bubble) {
    this.bubbles?.push({
      radius: random(this.minWidth * 2, this.maxWidth * 2),
      opacity: Math.random()
    })
  }
}

// 绘制气泡
function _drawBubble(instance: FreeLine, i: number, context: CanvasRenderingContext2D) {
  const b = instance.bubbles![i]
  const { x, y } = instance.positions[i]
  context.beginPath()
  context.globalAlpha = b.opacity
  context.arc(x, y, b.radius, 0, Math.PI * 2)
  context.fill()
}

效果:跟随鼠标画出半透明气泡,轻盈梦幻,适合儿童、治愈系绘画。


九、统一渲染入口

把 6 种特效整合到一个渲染函数:

typescript

运行

function freeDrawRender(context: CanvasRenderingContext2D, instance: FreeLine, texture: HTMLImageElement) {
  context.save()
  context.lineCap = 'round'
  context.lineJoin = 'round'

  switch (instance.style) {
    case FreeDrawStyle.Basic:
      context.strokeStyle = instance.colors[0]
      break
    case FreeDrawStyle.Fluorescent:
      context.strokeStyle = instance.colors[0]
      context.shadowColor = instance.colors[0]
      break
    case FreeDrawStyle.MultiColor:
      context.strokeStyle = getMultiColorPattern(instance.colors)
      break
    case FreeDrawStyle.Crayon:
      context.strokeStyle = getCrayonPattern(instance.colors[0], texture)
      break
    case FreeDrawStyle.Spray:
      context.fillStyle = instance.colors[0]
      break
    case FreeDrawStyle.Bubble:
      context.fillStyle = instance.colors[0]
      break
  }

  for (let i = 1; i < instance.positions.length; i++) {
    switch (instance.style) {
      case FreeDrawStyle.Basic:
      case FreeDrawStyle.Fluorescent:
      case FreeDrawStyle.MultiColor:
      case FreeDrawStyle.Crayon:
        _drawBasic(instance, i, context, () => {
          instance.style === FreeDrawStyle.Fluorescent &&
          (context.shadowBlur = instance.lineWidths[i])
        })
        break
      case FreeDrawStyle.Spray:
        _drawSpray(instance, i, context)
        break
      case FreeDrawStyle.Bubble:
        _drawBubble(instance, i, context)
        break
    }
  }
  context.restore()
}

十、效果展示

  • 基础单色:流畅、速度感应粗细
  • 荧光:发光霓虹效果
  • 多色:彩虹渐变一笔画
  • 喷绘:喷枪点阵质感
  • 蜡笔:纸质纹理真实感拉满
  • 气泡:半透明随机气泡

十一、优化与扩展建议

  1. 高清适配:使用 devicePixelRatio 提升高分屏清晰度
  2. 性能优化:离屏 Canvas 预渲染纹理、减少重复创建 Pattern
  3. 扩展笔触:增加毛笔、钢笔、水彩、油画等特效
  4. 数据持久化:将坐标序列 JSON 化,支持回放、存储、分享
  5. 压感支持:对接手写板、触屏压感,实现真实手绘力度感应

十二、总结

Canvas 实现绘画特效的核心在于:

  • 坐标记录 + 速度计算 实现手绘自然感
  • 贝塞尔曲线 保证线条流畅
  • shadow /pattern/ 随机点阵 实现不同视觉风格
  • 统一架构 让 6 种笔触可快速扩展、维护

本文实现的 6 种笔触,可直接集成到在线教育、创意画板、图文编辑、白板协作等项目中,代码轻量、易扩展、生产可用。

如果你也在做 Canvas 绘画相关项目,欢迎在评论区交流~