在前端创意绘画领域,Canvas 凭借像素级操控与高性能渲染,成为实现自由绘画、特效笔触的最佳方案。我在半年前开发了一款多功能 Canvas 绘画画板,支持自由绘制、荧光、彩虹、喷绘、蜡笔、气泡等 6 种特效,还具备橡皮擦、文字输入、无限拖拽、选区操作、图层管理、撤销重做与图片导出等完整能力。
最近我发现有人把核心绘画特效的设计思路与代码完整整理出来,这篇文章将带你从零实现一套可商用的 Canvas 特效绘画系统,全文包含原理讲解、完整代码、渲染效果,可直接用于项目开发。
Canvas大神 GitHub:github.com/LHRUN/paint…欢迎 Star 支持~
一、画板核心功能总览
这款 Canvas 绘画板具备专业绘图工具的完整能力:
- 自由绘制:支持颜色切换、根据手绘速度自动调整线条粗细
- 6 种笔触特效:基础单色、荧光、多色渐变、喷绘、蜡笔、气泡
- 橡皮擦:跟随鼠标线性擦除,支持擦除力度调节
- 文字绘制:双击画板添加文字
- 无限拖拽:按住空格拖拽画布,无边界创作
- 选区模式:框选元素,支持移动、缩放、删除
- 图层系统:支持添加、删除、排序图层
- 撤销 / 重做 / 清空 / 保存:完整工程化能力
本文重点讲解6 种绘画特效的实现原理与代码,从最简单的单色笔触,到视觉效果拉满的荧光、蜡笔、气泡,全部拆解。
二、基础准备:核心架构设计
在实现特效前,先统一基础架构:
- 鼠标移动时记录坐标点
- 根据移动速度计算动态线宽
- 使用贝塞尔曲线平滑线条
- 不同特效通过渲染函数分发
- 统一保存坐标、线宽、颜色、配置
基础数据结构:
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. 记录坐标并计算速度与线宽
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()
}
十、效果展示
- 基础单色:流畅、速度感应粗细
- 荧光:发光霓虹效果
- 多色:彩虹渐变一笔画
- 喷绘:喷枪点阵质感
- 蜡笔:纸质纹理真实感拉满
- 气泡:半透明随机气泡
十一、优化与扩展建议
- 高清适配:使用
devicePixelRatio提升高分屏清晰度 - 性能优化:离屏 Canvas 预渲染纹理、减少重复创建 Pattern
- 扩展笔触:增加毛笔、钢笔、水彩、油画等特效
- 数据持久化:将坐标序列 JSON 化,支持回放、存储、分享
- 压感支持:对接手写板、触屏压感,实现真实手绘力度感应
十二、总结
Canvas 实现绘画特效的核心在于:
- 坐标记录 + 速度计算 实现手绘自然感
- 贝塞尔曲线 保证线条流畅
- shadow /pattern/ 随机点阵 实现不同视觉风格
- 统一架构 让 6 种笔触可快速扩展、维护
本文实现的 6 种笔触,可直接集成到在线教育、创意画板、图文编辑、白板协作等项目中,代码轻量、易扩展、生产可用。
如果你也在做 Canvas 绘画相关项目,欢迎在评论区交流~