前言
我之前做了一个画板,曾经迭代了两个版本,但既然是画板,如果只有一种画笔就显得太枯燥了,我就收罗了一下网上的各种计划和本人的一些想法,目前做出了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