DEV Community

Cover image for Exploring the Canvas Series: Creative Brushes Part 3
Leo Song
Leo Song

Posted on

Exploring the Canvas Series: Creative Brushes Part 3

Introduction

I am currently developing a powerful open source creative drawing board. This drawing board contains a variety of creative brushes, which allows users to experience a new drawing effect. Whether on mobile or PC , you can enjoy a better interactive experience and effect display . And this project has many powerful auxiliary painting functions, including but not limited to forward and backward, copy and delete, upload and download, multiple boards and layers and so on. I'm not going to list all the detailed features, looking forward to your exploration.

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

Github: https://github.com/LHRUN/paint-board Welcome to Star ⭐️

preview

In the gradual development of the project, I plan to write some articles, on the one hand, to record the technical details, which is my habit all the time. On the other hand, I'd like to promote the project, and I hope to get your use and feedback, and of course, a Star would be my greatest support.

I'm going to explain the implementation of the Creative Brush in 3 articles, this is the third one, and I'll upload all the source code to my Github.

Github Source Code Demo

Multi Point Connection

  • The effect of multipoint connection is as follows

multiPoint

  • Multi-point connection is through the mouse in the coordinates of the movement of the randomly generated nearby points, and then the random points of the circle drawing, and each time will record the last movement of the point, and the two points of the line segment connection, you will have the above effect.
interface Point {
  x: number
  y: number
}

let isMouseDown = false
let lastPoints: Point[] = [] // Previous Array of random circular points

/**
 * Generate random points within a rectangle
 */
const generateRandomCoordinates = (
  centerX: number, // Rectangle Centre Point X
  centerY: number, // Rectangle Centre Point Y
  size: number, // Rectangle Size
  count: number // Number of generation
) => {
  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
      // Generate random points
      const points = generateRandomCoordinates(clientX, clientY, 50, 3)
      draw(points)
      lastPoints = points
    }
  }

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

    // If there is a previous random point, connect it.
    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();
      })
    }

    // Circular drawing of the current point
    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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Wave Brush

  • The wave curve effect is as follows

wiggle

  • Wave curve is in the mouse movement, according to the mouse to move the distance and angle of the two points for the semicircle drawing, and then the semicircle is every time will be flipped, so there will be a kind of wave effect
interface Point {
  x: number
  y: number
}

let isMouseDown = false
let movePoint: Point = { x: 0, y: 0 } // Moving Coordinate Recording
let flip = 1 // flip flag

/**
 * Get the distance between two points
 */
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

      // Get the distance between two points
      const distance = getDistance(movePoint, { x: clientX, y: clientY })

      // Get the middle point of the two points, which is also the centre of the semicircle.
      const midX = (movePoint.x + clientX) / 2
      const midY = (movePoint.y + clientY) / 2

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

      // Calculate the angle between two points
      const angle = Math.atan2(clientY - movePoint.y, clientX - movePoint.x)

      // Calculating whether to flip up or down
      const flipAngle = (flip % 2) * Math.PI

      // Drawing a semicircle
      context2D.arc(
        midX,
        midY,
        distance / 2,
        angle + flipAngle,
        angle + flipAngle + Math.PI
      );
      context2D.stroke();
      context2D.restore();

      // update data
      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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Thorn Brush

  • The thorn brush effect is as follows

thorn

  • Thorn brush is in the mouse movement, through the movement of the two points of the distance and angle of the ellipse drawing, because the height of the ellipse is relatively small, coupled with the width is calculated by the distance of movement, so there will be a kind of sharp effect
interface Point {
  x: number
  y: number
}

let isMouseDown = false
let movePoint: Point = { x: 0, y: 0 } // Moving Coordinate Recording
const minSize = 3 // Minimum width and height of ellipse 

/**
 * Get the distance between two points
 */
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
      // Get the distance between two points
      const distance = getDistance(movePoint, point)

      // Get the middle point of the two points, which is also the centre of the ellipse.
      const midX = (movePoint.x + clientX) / 2
      const midY = (movePoint.y + clientY) / 2

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

      // Calculate the angle between two points
      const angle = Math.atan2(clientY - movePoint.y, clientX - movePoint.x)

      /**
       * Drawing ellipse
       * height:  minSize
       * width: distance * 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Thank you for reading. This is the whole content of this article, I hope this article is helpful to you, welcome to like and favourite. If you have any questions, please feel free to discuss in the comment section!

Top comments (0)