前言

我之前做了一个画板,曾经迭代了两个版本,但既然是画板,如果只有一种画笔就显得太枯燥了,我就收罗了一下网上的各种计划和本人的一些想法,目前做出了5种款式,包含根底的总共6种,当然有了一些思路后,后续会持续减少。我会在本文具体阐明实现思路和具体代码,6种款式包含:

  1. 根底单色
  2. 荧光
  3. 多色画笔
  4. 喷雾
  5. 蜡笔
  6. 泡泡

预览

预览地址:https://songlh.top/paint-board/
源码:https://github.com/LHRUN/paint-board 欢送Star⭐️

根底单色

画笔的根底实现,除了点与点之间的连贯,还须要留神两点

  1. 首先是在鼠标挪动时计算以后挪动的速度,而后依据速度计算线宽,这个是为了实现鼠标挪动快,线宽就变窄,挪动慢,线宽就恢复正常这个成果
  2. 为了防止直线连接点成果不好,我会采纳贝塞尔曲线进行连贯

/** * 鼠标挪动时增加新的坐标 * @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}

泡泡

  1. 鼠标挪动时记录泡泡的半径和透明度
  2. 渲染时通过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