画板探秘系列:创意画笔第一期

前言

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

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

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

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

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

实现源码Demo

彩虹画笔

  • 彩虹画笔是会在绘制中不断变换颜色, 效果如下:
  • 这个效果相比于常规画笔只是多了一个颜色的转换, 我们知道常规画笔的实现是通过一个个线段连接而成, 无论你是曲线还是直线. 所以我们为了实现彩虹效果, 是需要改变每个线段的颜色, 改变线段颜色是通过修改 strokeStyle 属性
  • 然后改变 strokeStyle 颜色, 就不能简单的用常规的颜色表达, 我们需要知道一个知识 HSL
    • HSL 是一种颜色表达方式, 它通过圆柱坐标系来描述颜色, 分为色相(H), 饱和度(S), 亮度(L)
      • 色相(H): 表示颜色在色环上的位置
      • 饱和度(S): 它表示颜色的纯度或者说是灰度的程度。饱和度为 100% 表示完全饱和的颜色,而 0% 则表示灰度色。
      • 亮度(L): 它表示颜色的亮度。调整亮度可以改变颜色的明暗程度。
      • 具体概念可以看 MDN
      • HSL在线展示网站: mothereffinghsl.com/
  • 而我们只需要不断的调整 HSL 表达中的hue, 就可以达到颜色不断变换的效果
ts 复制代码
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() 进行生成图形路径, 然后绘制
ts 复制代码
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 就可以出现素材画笔的效果
ts 复制代码
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>
  )
}

像素画笔

  • 像素画笔效果如下
  • 像素画笔是通过鼠标移动, 在鼠标移动路径上, 随机根据点位进行矩形绘制, 多个矩形组合起来就有一种类似像素点的效果
ts 复制代码
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>
  )
}

总结

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

相关推荐
范文杰2 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪2 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪2 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy3 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom3 小时前
快速开始使用 n8n
后端·面试·github
uhakadotcom4 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom4 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom4 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom4 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom4 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试