头图

前言

我目前在维护一款功能强大的开源创意画板。这个画板集成了多种创意画笔,可以让用户体验到全新的绘画效果。无论是在移动端还是PC端,都能享受到较好的交互体验和效果展示。并且此项目拥有许多强大的辅助绘画功能,包括但不限于前进后退、复制删除、上传下载、多画板和多图层等等。详细功能我就不一一罗列了,期待你的探索。

Link: https://songlh.top/paint-board/

Github: https://github.com/LHRUN/paint-board 欢迎Star⭐️

在项目的逐渐迭代中,我计划撰写一些文章,一方面是为了记录技术细节,这是我一直以来的习惯。另一方面则是为了推广一下,期望得到你的使用和反馈,当然如果能点个 Star 就是对我最大的支持。

我准备分3篇文章讲解创意画笔的实现, 本篇文章是第一篇, 所有的实现源码我都会上传到我的 Github 上.

实现源码Demo

彩虹画笔

  • 彩虹画笔是会在绘制中不断变换颜色, 效果如下:

  • 这个效果相比于常规画笔只是多了一个颜色的转换, 我们知道常规画笔的实现是通过一个个线段连接而成, 无论你是曲线还是直线. 所以我们为了实现彩虹效果, 是需要改变每个线段的颜色, 改变线段颜色是通过修改 strokeStyle 属性
  • 然后改变 strokeStyle 颜色, 就不能简单的用常规的颜色表达, 我们需要知道一个知识 HSL

    • HSL 是一种颜色表达方式, 它通过圆柱坐标系来描述颜色, 分为色相(H), 饱和度(S), 亮度(L)

      • 色相(H): 表示颜色在色环上的位置
      • 饱和度(S): 它表示颜色的纯度或者说是灰度的程度。饱和度为 100% 表示完全饱和的颜色,而 0% 则表示灰度色。
      • 亮度(L): 它表示颜色的亮度。调整亮度可以改变颜色的明暗程度。
      • 具体概念可以看 MDN
      • HSL在线展示网站: https://mothereffinghsl.com/
  • 而我们只需要不断的调整 HSL 表达中的hue, 就可以达到颜色不断变换的效果
let hue = 0 // 记录当前的色相
let isMouseDown = false // 是否点击鼠标
let movePoint: { x: number, y: number } | null = null // 记录鼠标位置

function PaintBoard() {
  const canvasRef = useRef<HTMLCanvasElement | null>(null)
  const [context2D, setContext2D] = useState<CanvasRenderingContext2D | null>(null)

  useEffect(() => {
    if (canvasRef?.current) {
      const context2D = canvasRef?.current.getContext('2d')
      if (context2D) {
        context2D.lineCap = 'round'
        context2D.lineJoin = 'round'
        context2D.lineWidth = 10
        setContext2D(context2D)
      }
    }
  }, [canvasRef])

  const onMouseDown = () => {
    if (!canvasRef?.current || !context2D) {
      return
    }
    isMouseDown = true
  }

  const onMouseMove = (event: MouseEvent) => {
    if (!canvasRef?.current || !context2D) {
      return
    }
    if (isMouseDown) {
      const { clientX, clientY } = event
      if (movePoint) {
        /**
         * 在 0 到 360 的范围内, 逐步增加1, 但是当大于等于 360 时, 再回归为0
         */
        hue = hue < 360 ? hue + 1 : 0
        context2D.beginPath()
        // 通过 HSL 修改颜色
        context2D.strokeStyle = `hsl(${hue}, 90%, 50%)`
        context2D.moveTo(movePoint.x, movePoint.y)
        context2D.lineTo(clientX, clientY)
        context2D.stroke()
      }
      movePoint = {
        x: clientX,
        y: clientY
      }
    }
  }

  const onMouseUp = () => {
    if (!canvasRef?.current || !context2D) {
      return
    }
    isMouseDown = false
    movePoint = null
  }

  return (
    <div>
      <canvas
        ref={canvasRef}
        onMouseDown={onMouseDown}
        onMouseMove={onMouseMove}
        onMouseUp={onMouseUp}
      />
    </div>
  )
}

多形状画笔

  • 多形状画笔是会随着鼠标移动, 在移动路径上随机生成点位进行形状绘制, 效果如下

  • 实现方法, 通过每次鼠标移动的坐标, 在这个坐标的周围范围内随机生成几个点位, 然后在这些点位通过 new Path2D() 进行生成图形路径, 然后绘制
let isMouseDown = false

// 音乐符号形状路径
const musicPath = '***'

/**
 * 矩形内生成随机点位
 */
const generateRandomCoordinates = (
  centerX: number, // 矩形中心点 X
  centerY: number, // 矩形中心点 Y
  size: number, // 矩形大小
  count: number // 生成数量
) => {
  const halfSize = size / 2
  const points = []

  for (let i = 0; i < count; i++) {
    const randomX = Math.floor(centerX - halfSize + Math.random() * size)
    const randomY = Math.floor(centerY - halfSize + Math.random() * size)
    points.push({ x: randomX, y: randomY })
  }

  return points
}

function PaintBoard() {
  const canvasRef = useRef<HTMLCanvasElement | null>(null)
  const [context2D, setContext2D] = useState<CanvasRenderingContext2D | null>(null)

  useEffect(() => {
    if (canvasRef?.current) {
      const context2D = canvasRef?.current.getContext('2d')
      if (context2D) {
        context2D.fillStyle = '#000'
        setContext2D(context2D)
      }
    }
  }, [canvasRef])

  const onMouseDown = () => {
    if (!canvasRef?.current || !context2D) {
      return
    }
    isMouseDown = true
  }

  const onMouseMove = (event: MouseEvent) => {
    if (!canvasRef?.current || !context2D) {
      return
    }

    if (isMouseDown) {
      const { clientX, clientY } = event
      const points = generateRandomCoordinates(clientX, clientY, 30, 3)
      points.map((curPoint) => {
        createShape(curPoint.x, curPoint.y)
      })
    }
  }

  const createShape = (x: number, y: number) => {
    if (!context2D) {
      return
    }
    // 路径绘制
    const path = new Path2D(musicPath);
    context2D.beginPath();

    context2D.save();
    context2D.translate(x, y);

    // 形状随机缩放
    const scale = Math.random() * 1.5 + 0.5
    context2D.scale(scale, scale);

    context2D.fill(path);
    context2D.restore();
  }

  const onMouseUp = () => {
    if (!canvasRef?.current || !context2D) {
      return
    }
    isMouseDown = false
    moveDate = 0
  }

  return (
    <div>
      <canvas
        ref={canvasRef}
        onMouseDown={onMouseDown}
        onMouseMove={onMouseMove}
        onMouseUp={onMouseUp}
      />
    </div>
  )
}

素材画笔

  • 素材画笔效果如下

  • 素材画笔首先是需要一张透明的素材图, 这个图作为底图, 如果你用蜡笔材质的图片, 就会有蜡笔效果, 如果你用磨砂材质的图片, 就会有磨砂效果
  • 然后 strokeStyle 属性可以接收一个 CanvasPattern 对象, 详情可看 MDN
  • 我们可以新建一个 canvas, 然后对这个 canvas 绘制一张素材图片, 再绘制一个你需要的颜色, 最后通过这个 canvas 创建一个 pattern 赋值到 strokeStyle 就可以出现素材画笔的效果
let isMouseDown = false
let movePoint: { x: number, y: number } | null = null

// 加载所需素材图
const materialImage = new Promise<HTMLImageElement>((resolve) => {
  const image = new Image()
  image.src = '素材图地址'
  image.onload = () => {
    resolve(image)
  }
})

/**
 * 获取 pattern 对象
 * @param color 需要生成的颜色
 */
const getPattern = async (color: string) => {
  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)
  const image = await materialImage

  // 绘制素材图
  if (image) {
    context.drawImage(image, 0, 0, 100, 100)
  }
  return context.createPattern(canvas, 'repeat')
}

function PaintBoard() {
  const canvasRef = useRef<HTMLCanvasElement | null>(null)
  const [context2D, setContext2D] = useState<CanvasRenderingContext2D | null>(null)

  useEffect(() => {
    initDraw()
  }, [canvasRef])

  const initDraw = async () => {
    if (canvasRef?.current) {
      const context2D = canvasRef?.current.getContext('2d')
      if (context2D) {
        context2D.lineCap = 'round'
        context2D.lineJoin = 'round'
        context2D.lineWidth = 10
        // 获取 pattern 素材
        const pattern = await getPattern('blue')
        if (pattern) {
          context2D.strokeStyle = pattern
        }
        setContext2D(context2D)
      }
    }
  }

  const onMouseDown = () => {
    if (!canvasRef?.current || !context2D) {
      return
    }
    isMouseDown = true
  }

  const onMouseMove = (event: MouseEvent) => {
    if (!canvasRef?.current || !context2D) {
      return
    }
    if (isMouseDown) {
      const { clientX, clientY } = event
      if (movePoint) {
        // 画笔绘制
        context2D.beginPath()
        context2D.moveTo(movePoint.x, movePoint.y)
        context2D.lineTo(clientX, clientY)
        context2D.stroke()
      }
      movePoint = {
        x: clientX,
        y: clientY
      }
    }
  }

  const onMouseUp = () => {
    if (!canvasRef?.current || !context2D) {
      return
    }
    isMouseDown = false
    movePoint = null
  }

  return (
    <div>
      <canvas
        ref={canvasRef}
        onMouseDown={onMouseDown}
        onMouseMove={onMouseMove}
        onMouseUp={onMouseUp}
      />
    </div>
  )
}

像素画笔

  • 像素画笔效果如下

  • 像素画笔是通过鼠标移动, 在鼠标移动路径上, 随机根据点位进行矩形绘制, 多个矩形组合起来就有一种类似像素点的效果
let isMouseDown = false
const drawWidth = 15 // 像素画笔大小
const step = 5 // 每个像素点大小

function PaintBoard() {
  const canvasRef = useRef<HTMLCanvasElement | null>(null)
  const [context2D, setContext2D] = useState<CanvasRenderingContext2D | null>(null)

  useEffect(() => {
    if (canvasRef?.current) {
      const context2D = canvasRef?.current.getContext('2d')
      if (context2D) {
        context2D.fillStyle = '#000';
        setContext2D(context2D)
      }
    }
  }, [canvasRef])

  const onMouseDown = () => {
    if (!canvasRef?.current || !context2D) {
      return
    }
    isMouseDown = true
  }

  const onMouseMove = (event: MouseEvent) => {
    if (!canvasRef?.current || !context2D) {
      return
    }

    if (isMouseDown) {
      const { clientX, clientY } = event
      
      /**
       * 遍历当前像素画笔大小, 根据随机数判断是否绘制
       */
      for (let i = -drawWidth; i < drawWidth; i += step) {
        for (let j = -drawWidth; j < drawWidth; j += step) {
          if (Math.random() > 0.5) {
            context2D.save();
            context2D.fillRect(clientX + i, clientY + j, step, step);
            context2D.fill();
            context2D.restore();
          }
        }
      }
    }
  }

  const onMouseUp = () => {
    if (!canvasRef?.current || !context2D) {
      return
    }
    isMouseDown = false
  }

  return (
    <div>
      <canvas
        ref={canvasRef}
        onMouseDown={onMouseDown}
        onMouseMove={onMouseMove}
        onMouseUp={onMouseUp}
      />
    </div>
  )
}

总结

感谢你的阅读。以上就是本文的全部内容,希望这篇文章对你有所帮助,欢迎点赞和收藏。如果有任何问题,欢迎在评论区进行讨论


LH_S
121 声望6 粉丝

keep learning...