前言

最近空闲时间比较多,就想做点小工具玩玩,方案选了好几个,最终决定做一个基于canvas的画板,目前已经完成了第一版,有以下主要功能

  1. 画笔(动态宽度设置,颜色设置)
  2. 橡皮擦
  3. 撤回,反撤回,清除画板,保存
  4. 画板拖拽
  5. 多图层

预览

目前实现效果如下

预览地址:https://lhrun.github.io/paint-board/
repo:https://github.com/LHRUN/paint-board 欢迎star⭐️

画板设计

  1. 首先是建立一个canvas画板类,所有canvas上的操作和数据全都在此处理,例如初始化,渲染,拖拽画板等等
class PaintBoard {canvas: HTMLCanvasElementcontext: CanvasRenderingContext2D...constructor(canvas: HTMLCanvasElement) {}// 初始化canvasinitCanvas() {}// 渲染render() {}// 拖拽drag() {}...
}
  1. 然后基于canvas类,根据当前的操作,建立对应的canvas元素,比如画笔,橡皮擦,基本类型如下
class CanvasElement {type: string // 元素类型layer: number // 图层// ...constructor(type: string, layer: number) {this.type = typethis.layer = layer// ...}// ...
}
  1. 最后根据渲染逻辑,还会封装一些通用的逻辑来改变canvas上最终的展示,比如撤回,反撤回,图层操作等等

画笔

  • 实现画笔效果首先要在鼠标按下时建立一个画笔元素,然后在构造函数中接受基础宽度,颜色,初始化鼠标移动记录和线宽记录,然后在鼠标移动时记录鼠标移动的坐标
  • 为了体现鼠标移动快,线宽就变窄,移动慢,线宽就恢复正常这个效果,我会计算当前移动的速度,然后根据速度计算线宽
class FreeLine extends CanvasElement {...constructor(color: string, width: number, layer: number) {this.positions = [] // 鼠标移动位置记录this.lineWidths = [0] // 线宽记录this.color = color // 当前绘线颜色this.maxWidth = width // 最大线宽this.minWidth = width / 2 // 最小线宽this.lastLineWidth = width // 最后绘线宽度}
}
  • 记录鼠标位置和当前线宽
interface MousePosition {x: numbery: number
}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 = curTimereturn mouseSpeed
}/*** 计算画笔宽度* @param speed 鼠标移动速度*/
_computedLineWidth(speed: number) {let lineWidth = 0const minWidth = this.minWidthconst maxWidth = this.maxWidthif (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 = lineWidthreturn lineWidth
}
  • 保存坐标后,渲染就是遍历所有坐标
function freeLineRender(context: CanvasRenderingContext2D,instance: FreeLine
) {context.save()context.lineCap = 'round'context.lineJoin = 'round'context.strokeStyle = instance.colorfor (let i = 1; i < instance.positions.length; i++) {_drawLine(instance, i, context)}context.restore()
}/*** 画笔轨迹是借鉴了网上的一些方案,分两种情况* 1. 如果是前两个坐标,就通过lineTo连接即可* 2. 如果是前两个坐标之后的坐标,就采用贝塞尔曲线进行连接,*    比如现在有a, b, c 三个点,到c点时,把ab坐标的中间点作为起点*     bc坐标的中间点作为终点,b点作为控制点进行连接*/
function _drawLine(instance: FreeLine,i: number,context: CanvasRenderingContext2D
) {const { positions, lineWidths } = instanceconst { 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) / 2const lastY = (startY + centerY) / 2const x = (centerX + endX) / 2const y = (centerY + endY) / 2context.moveTo(lastX, lastY)context.quadraticCurveTo(centerX, centerY, x, y)}context.lineWidth = lineWidths[i]context.stroke()
}

橡皮擦

  • 橡皮擦是一个线状擦除,我采用的方案是通过计算每个点的圆弧轨迹和两个点之间的矩形区域,然后通过clip剪切后清除
/*** 橡皮擦渲染* @param context canvas二维渲染上下文* @param cleanCanvas 清除画板* @param instance CleanLine*/
function cleanLineRender(context: CanvasRenderingContext2D,cleanCanvas: () => void,instance: CleanLine
) {for (let i = 0; i < instance.positions.length - 1; i++) {_cleanLine(instance.positions[i],instance.positions[i + 1],context,cleanCanvas,instance.cleanWidth)}
}/*** 线状清除* @param start 起点* @param end 终点* @param context canvas二维渲染上下文* @param cleanCanvas 清除画板* @param cleanWidth 清楚宽度*/
function _cleanLine(start: MousePosition,end: MousePosition,context: CanvasRenderingContext2D,cleanCanvas: () => void,cleanWidth: number
){const { x: x1, y: y1 } = startconst { x: x2, y: y2 } = end// 获取鼠标起点和终点之间的矩形区域端点const asin = cleanWidth * Math.sin(Math.atan((y2 - y1) / (x2 - x1)))const acos = cleanWidth * Math.cos(Math.atan((y2 - y1) / (x2 - x1)))const x3 = x1 + asinconst y3 = y1 - acosconst x4 = x1 - asinconst y4 = y1 + acosconst x5 = x2 + asinconst y5 = y2 - acosconst x6 = x2 - asinconst y6 = y2 + acos// 清除末端圆弧context.save()context.beginPath()context.arc(x2, y2, cleanWidth, 0, 2 * Math.PI)context.clip()cleanCanvas()context.restore()// 清除矩形区域context.save()context.beginPath()context.moveTo(x3, y3)context.lineTo(x5, y5)context.lineTo(x6, y6)context.lineTo(x4, y4)context.closePath()context.clip()cleanCanvas()context.restore()
}

撤回、反撤回

  • 实现撤回,反撤回就要把canvas上的每个元素的渲染数据进行存储,通过改变控制变量,限制渲染元素的遍历,这样就可以达到撤回的效果
  • 首先画板初始化时建立一个history类,然后建立缓存和step数据,撤回和反撤回时,只需要修改step即可
class History<T> {cacheQueue: T[]step: numberconstructor(cacheQueue: T[]) {this.cacheQueue = cacheQueuethis.step = cacheQueue.length - 1}// 添加数据add(data: T) {// 如果在回退时添加数据就删除暂存数据if (this.step !== this.cacheQueue.length - 1) {this.cacheQueue.length = this.step + 1}this.cacheQueue.push(data)this.step = this.cacheQueue.length - 1}// 遍历cacheQueueeach(cb?: (ele: T, i: number) => void) {for (let i = 0; i <= this.step; i++) {cb?.(this.cacheQueue[i], i)}}// 后退undo() {if (this.step >= 0) {this.step--return this.cacheQueue[this.step]}}// 前进redo() {if (this.step < this.cacheQueue.length - 1) {this.step++return this.cacheQueue[this.step]}}
}
  • 针对画板,通过监听鼠标按下操作,在history中添加一个元素,然后对渲染函数的遍历限制到step就达到了撤回的效果
class PaintBoard {.../*** 记录当前元素,并加入history*/recordCurrent(type: string) {let ele: ELEMENT_INSTANCE | null = nullswitch (type) {case CANVAS_ELE_TYPE.FREE_LINE:ele = new FreeLine(this.currentLineColor,this.currentLineWidth,this.layer.current)breakcase CANVAS_ELE_TYPE.CLEAN_LINE:ele = new CleanLine(this.cleanWidth, this.layer.current)breakdefault:break}if (ele) {this.history.add(ele)this.currentEle = ele}}/*** 遍历history渲染数据*/render() {// 清除画布this.cleanCanvas()// 遍历historythis.history.each((ele) => {this.context.save()// render....this.context,resore()})// 缓存数据this.cache()}
}

拖拽画布

  • 拖拽画布的实现是通过计算鼠标移动距离,根据距离改变画布的原点位置,达到拖拽的效果
function drag(position: MousePosition) {const mousePosition = {x: position.x - this.canvasRect.left,y: position.y - this.canvasRect.top}if (this.originPosition.x && this.originPosition.y) {const translteX = mousePosition.x - this.originPosition.xconst translteY = mousePosition.y - this.originPosition.ythis.context.translate(translteX, translteY)this.originTranslate = {x: translteX + this.originTranslate.x,y: translteY + this.originTranslate.y}this.render()}this.originPosition = mousePosition
}

多图层

实现多图层需要对以下几个地方进行处理

  1. 画板初始化时建立图层类,所有的图层数据和图层逻辑全在此处
  2. 然后对canvas上的元素加layer属性,用于判断归属于哪个图层
  3. 画板的渲染函数改为按照图层顺序进行渲染
  4. 拖拽或者隐藏图层都需要重新渲染,删除图层把对应的缓存图层元素进行删除
interface ILayer {id: number // 图层idtitle: string // 图层名称show: boolean // 图层展示状态
}/*** 图层*/
class Layer {stack: ILayer[] // 图层数据current: number // 当前图层render: () => void // 画板渲染事件constructor(render: () => void, initData?: Layer) {const {stack = [{id: 1,title: 'item1',show: true}],id = 1,current = 1} = initData || {}this.stack = stackthis.id = idthis.current = currentthis.render = render}...
}class PaintBoard {// 通过图层进行排序sortOnLayer() {this.history.sort((a, b) => {return (this.layer.stack.findIndex(({ id }) => id === b?.layer) -this.layer.stack.findIndex(({ id }) => id === a?.layer))})}// 渲染函数只渲染图层展示状态的元素render() {const showLayerIds = new Set(this.layer.stack.reduce<number[]>((acc, cur) => {return cur.show ? [...acc, cur.id] : acc}, []))this.history.each((ele) => {if (ele?.layer && showLayerIds.has(ele.layer)) {...}} }
}

总结

  • 我本篇主要是分享一些主要逻辑,还有一些兼容问题和一些UI交互就不叙述了
  • 这个画板写下来大概用了一个星期,有好多功能还没写上,如果过段时间有空的话就继续写下去,并进一步优化,现在还是有点优化问题没有写好,比如画笔宽度显示的还是有点问题,原点位置和一些初始化设计的不太好,不过写完这个画板还是挺有成就感的

参考资料

  • HTML5 实现橡皮擦的擦除效果
  • 我做了一个在线白板!

基于canvas实现的多功能画板相关推荐

  1. 基于canvas的超级画板

    最近不忙,于是写了个canvas 画板,网上的画板,大多功能单一,有时候无法满足我们的绘画欲望.这款画板集合了好多功能,比如滤镜功能,旋转功能,像素复制,多边形,多较形,编辑图片,自定义渐变颜色... ...

  2. 基于canvas剪辑区域功能实现橡皮擦效果

    这篇文章主要介绍了基于canvas剪辑区域功能实现橡皮擦效果,非常不错,具有参考借鉴价值,需要的朋友可以参考下 这是基础结构 没什么好说的 ?<!DOCTYPE html> <htm ...

  3. java做橡皮擦效果_基于canvas剪辑区域功能实现橡皮擦效果

    这篇文章主要介绍了基于canvas剪辑区域功能实现橡皮擦效果,非常不错,具有参考借鉴价值,需要的朋友可以参考下 这是基础结构 没什么好说的 ? Document *{padding: 0;margin ...

  4. html中钟表功能的js插件,基于canvas的15种不同外观时钟js插件

    CanvasClock是一款基于Canvas的纯js时钟插件.该js时钟插件可以通过配置参数来生成15种不同外观的时钟.它使用纯js来制作,没有使用任何css代码和外部依赖. 使用方法 在页面引入ca ...

  5. 7个华丽的基于Canvas的HTML5动画

    说起HTML5,可能让你印象更深的是其基于Canvas的动画特效,虽然Canvas在HTML5中的应用并不全都是动画制作,但其动画效果确实让人震惊.本文收集了7个最让人难忘的HTML5 Canvas动 ...

  6. 微信小程序-基于canvas画画涂鸦

    代码地址如下: http://www.demodashi.com/demo/14461.html 一.前期准备工作 软件环境:微信开发者工具 官方下载地址:https://mp.weixin.qq.c ...

  7. canvas换图时候会闪烁_基于Canvas实现的高斯模糊(上)「JS篇」

    作者:iNahoo 转发链接:https://mp.weixin.qq.com/s/5TxPjznpEBku_ybSMBdnfw 目录 基于Canvas实现的高斯模糊(上)「JS篇」本篇 基于Canv ...

  8. html5可视化图形编辑器(基于canvas)

    我以前特别喜欢flash,不过flash水平一般,那是的我并不是程序员,充其量也就是个爱好者,在这个html5的时代中,我依旧对那个有时间轴的flash编辑界面念念不忘.于是便有了这篇文章.我的目标是 ...

  9. 基于canvas的视频遮罩插件

    作者:云荒杯倾 作者博客 视频遮罩介绍 为一个视频添加一个覆盖物,从而挡住视频某区域,在视频的某一时间段,比如第10到第20分钟不显示划定的这块区域.应用场景包括遮挡卫视图标.遮挡视频右下角广告.充当 ...

最新文章

  1. [分享]C# 获取Outlook帐号和密码
  2. 「SAP技术」SAP WM 如何根据TR号码查询TO号码?
  3. (转)Ubuntu10.04各文件夹的作用
  4. python not函数_python 函数
  5. css 外弧_css 伪类实现弧形
  6. 推荐系统和搜索引擎的关系
  7. 被单位开除,已经交了14年的养老保险,该怎么办?
  8. Angular 导致公司损失数十亿美元!
  9. IO基础操作(文件)
  10. 下面列出LoadRunner的性能测试流程
  11. 黑苹果常用 工具+Kext+ACPI+UEFI驱动 下载
  12. 实现lightbox效果
  13. 基于消防GIS系统的智慧消防应用
  14. 在线教育项目-npm install失败-下载依赖失败-(vue-admin-template-master)
  15. 树莓派4B:连接windows远程桌面
  16. 脸书隐藏了未能阻止滥用技术的官僚主义报道的失败
  17. javascript event click/dblclick left/right区分左键、右键、双击事件,排除点击事件与拖拽事件冲突,做防抖优化
  18. 算法与数据结构实验题 10.23 寡人的难题
  19. 说说海龟交易法则的基本原理,如何实现海龟交易策略?
  20. 嵌入式课程---嵌入式Linux的直流电机驱动开发

热门文章

  1. Python 实现 周志华 《机器学习》 BP算法
  2. 使用sklearn库进行数据标准化处理
  3. 亲自传授我的各种经典的篮球技术动作gif图
  4. linux ide sata硬盘,Linux 下SATA与IDE硬盘区别
  5. Python 二维字典定义
  6. 【iOS开发】-UIPickerView
  7. 美业SaaS的创业分享之[定位]:美业SaaS的定位到底是工具还是平台
  8. CGLib中类Enhancer介绍
  9. 最新notion enhancer安装教程(macOS Intel适用)
  10. 如何进行智慧城市顶层设计规划