前言
我之前做了一个画板,曾经迭代了两个版本,但既然是画板,如果只有一种画笔就显得太枯燥了,我就收罗了一下网上的各种计划和本人的一些想法,目前做出了 5 种款式,包含根底的总共 6 种,当然有了一些思路后,后续会持续减少。我会在本文具体阐明实现思路和具体代码,6 种款式包含:
- 根底单色
- 荧光
- 多色画笔
- 喷雾
- 蜡笔
- 泡泡
预览
预览地址:https://songlh.top/paint-board/
源码:https://github.com/LHRUN/paint-board 欢送 Star⭐️
根底单色
画笔的根底实现,除了点与点之间的连贯,还须要留神两点
- 首先是在鼠标挪动时计算以后挪动的速度,而后依据速度计算线宽,这个是为了实现鼠标挪动快,线宽就变窄,挪动慢,线宽就恢复正常这个成果
- 为了防止直线连接点成果不好,我会采纳贝塞尔曲线进行连贯
/**
* 鼠标挪动时增加新的坐标
* @param position
*/
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)
}
}
/**
* 计算挪动速度
* @param start 终点
* @param end 起点
*/
_computedSpeed(start: MousePosition, end: MousePosition) {
// 获取间隔
const moveDistance = getDistance(start, end)
const curTime = Date.now()
// 获取挪动间隔时间 lastMoveTime:最初鼠标挪动工夫
const moveTime = curTime - this.lastMoveTime
// 计算速度
const mouseSpeed = moveDistance / moveTime
// 更新最初挪动工夫
this.lastMoveTime = curTime
return mouseSpeed
}
/**
* 计算画笔宽度
* @param speed 鼠标挪动速度
*/
_computedLineWidth(speed: number) {
let lineWidth = 0
const minWidth = this.minWidth
const maxWidth = this.maxWidth
if (speed >= this.maxSpeed) {lineWidth = minWidth} else if (speed <= this.minSpeed) {lineWidth = maxWidth} else {lineWidth = maxWidth - (speed / this.maxSpeed) * maxWidth
}
lineWidth = lineWidth * (1 / 3) + this.lastLineWidth * (2 / 3)
this.lastLineWidth = lineWidth
return lineWidth
}
渲染时就遍历所有坐标
/**
* 自在画笔渲染
* @param context canvas 二维渲染上下文
* @param instance FreeDraw
*/
function freeDrawRender(
context: CanvasRenderingContext2D,
instance: FreeLine
) {context.save()
context.lineCap = 'round'
context.lineJoin = 'round'
switch (instance.style) {
// 当初是只有根底画笔,后续会减少不同的 case
case FreeDrawStyle.Basic:
context.strokeStyle = instance.colors[0]
break
default:
break
}
for (let i = 1; i < instance.positions.length; i++) {switch (instance.style) {
case FreeDrawStyle.Basic:
_drawBasic(instance, i, context)
break
default:
break
}
}
context.restore()}
/**
* 绘制根底线条
* @param instance FreeDraw 实例
* @param i 下标
* @param context canvas 二维渲染上下文
* @param cb 一些绘制前的解决,批改一些款式
*
* 画笔轨迹是借鉴了网上的一些计划,分两种状况
* 1. 如果是前两个坐标,就通过 lineTo 连贯即可
* 2. 如果是前两个坐标之后的坐标,就采纳贝塞尔曲线进行连贯,* 比方当初有 a, b, c 三个点,到 c 点时,把 ab 坐标的两头点作为终点
* bc 坐标的两头点作为起点,b 点作为控制点进行连贯
*/
function _drawBasic(
instance: FreeLine,
i: number,
context: CanvasRenderingContext2D
cb?: (
instance: FreeDraw,
i: number,
context: CanvasRenderingContext2D
) => void
) {const { positions, lineWidths} = instance
const {x: centerX, y: centerY} = positions[i - 1]
const {x: endX, y: endY} = positions[i]
context.beginPath()
if (i == 1) {context.moveTo(centerX, centerY)
context.lineTo(endX, endY)
} else {const { x: startX, y: startY} = positions[i - 2]
const lastX = (startX + centerX) / 2
const lastY = (startY + centerY) / 2
const x = (centerX + endX) / 2
const y = (centerY + endY) / 2
context.moveTo(lastX, lastY)
context.quadraticCurveTo(centerX, centerY, x, y)
}
context.lineWidth = lineWidths[i]
cb?.(instance, i, context)
context.stroke()}
荧光
荧光只需在根底款式上减少一个暗影即可
function freeDrawRender(
context: CanvasRenderingContext2D,
instance: FreeLine
) {context.save()
context.lineCap = 'round'
context.lineJoin = 'round'
switch (instance.style) {
// ...
// 荧光 减少暗影成果
case FreeDrawStyle.Shadow:
context.shadowColor = instance.colors[0]
context.strokeStyle = instance.colors[0]
break
default:
break
}
for (let i = 1; i < instance.positions.length; i++) {switch (instance.style) {
// ...
// 荧光
case FreeDrawStyle.Shadow:
_drawBasic(instance, i, context, (instance, i, context) => {context.shadowBlur = instance.lineWidths[i]
})
break
default:
break
}
}
context.restore()}
多色画笔
多色画笔须要应用context.createPattern
,这个 api 是能够通过 canvas 创立一个指定的模版,而后能够让这个模版在指定的方向上反复元图像,具体应用能够看 MDN
/**
* 自在画笔渲染
* @param context canvas 二维渲染上下文
* @param instance FreeDraw
* @param material 画笔素材
*/
export const freeDrawRender = (
context: CanvasRenderingContext2D,
instance: FreeDraw,
material: Material
) => {context.save()
context.lineCap = 'round'
context.lineJoin = 'round'
switch (instance.style) {
// ...
// 多色画笔
case FreeDrawStyle.MultiColor:
context.strokeStyle = getMultiColorPattern(instance.colors)
break
default:
break
}
for (let i = 1; i < instance.positions.length; i++) {switch (instance.style) {
// ...
// 多色画笔
case FreeDrawStyle.MultiColor:
_drawBasic(instance, i, context)
break
default:
break
}
}
context.restore()}
/**
* 获取多色模版
* @param colors 多色数组
*/
const getMultiColorPattern = (colors: string[]) => {const canvas = document.createElement('canvas')
const context = canvas.getContext('2d') as CanvasRenderingContext2D
const COLOR_WIDTH = 5 // 每个色彩的宽度
canvas.width = COLOR_WIDTH * colors.length
canvas.height = 20
colors.forEach((color, i) => {
context.fillStyle = color
context.fillRect(COLOR_WIDTH * i, 0, COLOR_WIDTH, 20)
})
return context.createPattern(canvas, 'repeat') as CanvasPattern
}
喷雾
喷雾是一种相似雪花的成果,在鼠标挪动门路上随机绘制,然而最后我在写的时候发现,如果对每个点都进行随机雪花点记录而后缓存下来,内存占用过多,我就尝试了提前生成 5 套不同的数据,按程序展现,也能达到随机的成果
export const freeDrawRender = (
context: CanvasRenderingContext2D,
instance: FreeDraw,
material: Material
) => {context.save()
context.lineCap = 'round'
context.lineJoin = 'round'
switch (instance.style) {
// ...
// 喷雾
case FreeDrawStyle.Spray:
context.fillStyle = instance.colors[0]
break
default:
break
}
for (let i = 1; i < instance.positions.length; i++) {switch (instance.style) {
// ...
// 喷雾
case FreeDrawStyle.Spray:
_drawSpray(instance, i, context)
break
default:
break
}
}
context.restore()}
/**
* 绘制喷雾
* @param instance FreeDraw 实例
* @param i 下标
* @param context canvas 二维渲染上下文
*/
const _drawSpray = (
instance: FreeDraw,
i: number,
context: CanvasRenderingContext2D
) => {const { x, y} = instance.positions[i]
for (let j = 0; j < 50; j++) {
/**
* sprayPoint 是我提前生成的 5 套随机喷雾数据,按程序展现
* {
* angle 弧度
* radius 半径
* alpha 透明度
* }
*/
const {angle, radius, alpha} = sprayPoint[i % 5][j]
context.globalAlpha = alpha
const distanceX = radius * Math.cos(angle)
const distanceY = radius * Math.sin(angle)
// 依据宽度限度喷雾宽度,因为喷雾太细了不难看,我就对立放大一倍
if (distanceX < instance.lineWidths[i] * 2 &&
distanceY < instance.lineWidths[i] * 2 &&
distanceX > -instance.lineWidths[i] * 2 &&
distanceY > -instance.lineWidths[i] * 2
) {context.fillRect(x + distanceX, y + distanceY, 2, 2)
}
}
}
蜡笔
蜡笔成果也是应用了context.createPattern
,首先我是以以后画笔色彩为底色,而后通过在网上找的一张蜡笔材质的透明图笼罩在下面,就能够实现蜡笔的成果
/**
* 自在画笔渲染
* @param context canvas 二维渲染上下文
* @param instance FreeDraw
* @param material 画笔素材
*/
export const freeDrawRender = (
context: CanvasRenderingContext2D,
instance: FreeDraw,
material: Material
) => {context.save()
context.lineCap = 'round'
context.lineJoin = 'round'
switch (instance.style) {
// ...
// 蜡笔
case FreeDrawStyle.Crayon:
context.strokeStyle = getCrayonPattern(instance.colors[0],
material.crayon
)
break
default:
break
}
for (let i = 1; i < instance.positions.length; i++) {switch (instance.style) {
// ...
// 蜡笔
case FreeDrawStyle.Crayon:
_drawBasic(instance, i, context)
break
default:
break
}
}
context.restore()}
/**
* 获取蜡笔模版
* @param color 蜡笔底色
* @param crayon 蜡笔素材
*/
const getCrayonPattern = (color: string, crayon: Material['crayon']) => {const canvas = document.createElement('canvas')
const context = canvas.getContext('2d') as CanvasRenderingContext2D
canvas.width = 100
canvas.height = 100
context.fillStyle = color
context.fillRect(0, 0, 100, 100)
if (crayon) {context.drawImage(crayon, 0, 0, 100, 100)
}
return context.createPattern(canvas, 'repeat') as CanvasPattern
}
泡泡
- 鼠标挪动时记录泡泡的半径和透明度
- 渲染时通过
context.arc
进行画圆绘制
addPosition(position: MousePosition) {
// ...
// 记录泡泡半径和透明度
if (this.style === FreeDrawStyle.Bubble && this.bubbles) {
this.bubbles.push({
// getRandomInt 获取范畴内随机整数
radius: getRandomInt(this.minWidth * 2, this.maxWidth * 2),
// 透明度
opacity: Math.random()})
}
// ...
}
/**
* 绘制泡泡
* @param instance FreeDraw 实例
* @param i 下标
* @param context canvas 二维渲染上下文
*/
const _drawBubble = (
instance: FreeDraw,
i: number,
context: CanvasRenderingContext2D
) => {context.beginPath()
if (instance.bubbles) {const { x, y} = instance.positions[i]
context.globalAlpha = instance.bubbles[i].opacity
context.arc(x, y, instance.bubbles[i].radius, 0, Math.PI * 2, false)
context.fill()}
}
总结
如果有发现问题或者有好的计划,欢送探讨👻
画板系列文章:
- 基于 canvas 实现的多功能画板
- canvas 画板之绘画元素的框选
参考资料
- Exploring canvas drawing techniques