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

前言

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

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

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

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

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

实现源码Demo

多点连接

  • 多点连接效果如下

  • 多点连接是通过对鼠标移动中坐标进行随机生成附近点位, 然后对随机点位进行圆形绘制, 并且每次都会记录上一次移动的点位, 并对两个点位进行线段连接, 就会有以上效果.
interface Point {
  x: number
  y: number
}

let isMouseDown = false
let lastPoints: Point[] = [] // 上一个随机圆形绘制点位数组

/**
 * 矩形内生成随机点位
 */
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 = '#000000';
        context2D.strokeStyle = '#000000';
        context2D.lineWidth = 1;
        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, 50, 3)
      draw(points)
      lastPoints = points
    }
  }

  const draw = (points: Point[]) => {
    if (!context2D) {
      return
    }

    // 如果有上一个随机点位, 就进行直线连接
    if (lastPoints.length) {
      lastPoints.forEach(({ x, y }, index) => {
        context2D.beginPath();
        context2D.save();
        context2D.moveTo(x, y);
        context2D.lineTo(points[index].x, points[index].y);
        context2D.stroke();
        context2D.restore();
      })
    }

    // 对当前的随机点位进行圆形绘制
    points.map((curPoint) => {
      context2D.beginPath();
      context2D.save();
      context2D.arc(curPoint.x, curPoint.y, 7, 0, 2 * Math.PI, false);
      context2D.fill();
      context2D.restore();
    })
  }

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

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

export default PaintBoard

波浪曲线

  • 波浪曲线效果如下

  • 波浪曲线是在鼠标移动中, 根据鼠标移动的两点距离和角度进行半圆绘制, 然后半圆是每一次都会进行翻转, 所以就会有一种波浪的效果, 并且移动快就半圆变大, 移动慢就半圆变小
interface Point {
  x: number
  y: number
}

let isMouseDown = false
let movePoint: Point = { x: 0, y: 0 } // 移动坐标记录
let flip = 1 // 翻转记录

/**
 * 根据勾股定理得出距离
 */
const getDistance = (start: Point, end: Point) => {
  return Math.sqrt(Math.pow(start.x - end.x, 2) + Math.pow(start.y - end.y, 2))
}

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.strokeStyle = '#000'
        context2D.lineJoin = 'round'
        context2D.lineCap = 'round'
        setContext2D(context2D)
      }
    }
  }, [canvasRef])

  const onMouseDown = (event: MouseEvent) => {
    if (!canvasRef?.current || !context2D) {
      return
    }
    isMouseDown = true
    const { clientX, clientY } = event
    movePoint = {
      x: clientX,
      y: clientY
    }
  }

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

    if (isMouseDown) {
      const { clientX, clientY } = event

      // 获取两点距离
      const distance = getDistance(movePoint, { x: clientX, y: clientY })

      // 得出两点的中间点, 这个也是半圆的圆心
      const midX = (movePoint.x + clientX) / 2
      const midY = (movePoint.y + clientY) / 2

      context2D.beginPath();
      context2D.save();

      // 计算两点的角度
      const angle = Math.atan2(clientY - movePoint.y, clientX - movePoint.x)

      // 计算是上翻转还是下翻转
      const flipAngle = (flip % 2) * Math.PI

      // 绘制半圆
      context2D.arc(
        midX,
        midY,
        distance / 2,
        angle + flipAngle,
        angle + flipAngle + Math.PI
      );
      context2D.stroke();
      context2D.restore();

      // 更新数据
      flip++;
      movePoint = {
        x: clientX,
        y: clientY
      }
    }
  }

  const onMouseUp = () => {
    if (!canvasRef?.current || !context2D) {
      return
    }
    isMouseDown = false
    movePoint = { x: 0, y: 0 }
  }

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

荆棘画笔

  • 荆棘画笔效果如下

  • 荆棘画笔是在鼠标移动中, 通过移动的两点距离和角度进行椭圆的绘制, 因为椭圆的高度比较小, 再加上宽度是通过移动距离计算, 所以就会有一种尖锐的效果
interface Point {
  x: number
  y: number
}

let isMouseDown = false
let movePoint: Point = { x: 0, y: 0 } // 鼠标点位记录
const minSize = 3 // 椭圆最小宽高 

/**
 * 根据勾股定理得出距离
 */
const getDistance = (start: Point, end: Point) => {
  return Math.sqrt(Math.pow(start.x - end.x, 2) + Math.pow(start.y - end.y, 2))
}

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'
        context2D.lineJoin = 'round'
        context2D.lineCap = 'round'
        setContext2D(context2D)
      }
    }
  }, [canvasRef])

  const onMouseDown = (event: MouseEvent) => {
    if (!canvasRef?.current || !context2D) {
      return
    }
    isMouseDown = true
    const { clientX, clientY } = event
    movePoint = {
      x: clientX,
      y: clientY
    }
  }

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

    if (isMouseDown) {
      const { clientX, clientY } = event
      // 根据两点得出距离
      const distance = getDistance(movePoint, point)

      // 得出两点的中间点, 这个也是椭圆的圆心
      const midX = (movePoint.x + clientX) / 2
      const midY = (movePoint.y + clientY) / 2

      context2D.beginPath();
      context2D.save();

      // 得出移动的角度
      const angle = Math.atan2(clientY - movePoint.y, clientX - movePoint.x)

      /**
       * 绘制椭圆
       * 高度固定 minSize
       * 宽度为 距离 * 5 + minSize
       */
      context2D.ellipse(
        midX,
        midY,
        distance * 5 + minSize,
        minSize,
        angle,
        0,
        2 * Math.PI
      );
      context2D.fill();
      context2D.restore();

      movePoint = {
        x: clientX,
        y: clientY
      }
    }
  }

  const onMouseUp = () => {
    if (!canvasRef?.current || !context2D) {
      return
    }
    isMouseDown = false
    movePoint = { x: 0, y: 0 }
  }

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

总结

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