Exploring the Canvas Series: Creative Brushes Part 2 (original) (raw)

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 ⭐️

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 second one, and I’ll upload all the source code to my Github.

Github Source Code Demo

Multi Colour Brush

import { useEffect, useRef, useState, MouseEvent } from 'react'
import './index.css'

let isMouseDown = false
let movePoint: { x: number, y: number } | null = null
const COLOR_WIDTH = 5 // width of each colour

/**
 * get multicolour brush pattern
 * @param colors Colour array, colours to be painted
 */
const getPattern = async (colors: string[]) => {
  const canvas = document.createElement('canvas')
  const context = canvas.getContext('2d') as CanvasRenderingContext2D
  renderRow(canvas, context, colors)
  return context.createPattern(canvas, 'repeat')
}

/**
 * row effect drawing
 */
const renderRow = (
  canvas: HTMLCanvasElement,
  context: CanvasRenderingContext2D,
  colors: string[]
) => {
  canvas.width = 20
  canvas.height = colors.length * COLOR_WIDTH
  colors.forEach((color, i) => {
    context.fillStyle = color
    context.fillRect(0, COLOR_WIDTH * i, 20, COLOR_WIDTH)
  })
}

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
        // Assigns a value to strokeStyle based on the generated pattern
        const pattern = await getPattern(['blue', 'red', 'black'])
        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>
  )
}

Text Brush

interface Point {
  x: number
  y: number
}

let isMouseDown = false
let movePoint: Point = { x: 0, y: 0 }

let counter = 0 // currently drawing text
const textValue = 'PaintBoard' // Drawing text content
const minFontSize = 5 // min fontsize

/**
 * 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'
        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 })
      const fontSize = minFontSize + distance
      const letter = textValue[counter]
      context2D.font = `${fontSize}px Georgia`

      // Get text width
      const textWidth = context2D.measureText(letter).width

      if (distance > textWidth) {
        // Calculate the current movement angle
        const angle = Math.atan2(clientY - movePoint.y, clientX - movePoint.x)

        context2D.save();
        context2D.translate(movePoint.x, movePoint.y)
        context2D.rotate(angle);
        context2D.fillText(letter, 0, 0);
        context2D.restore();

        // Update the position of the text after drawing
        movePoint = {
          x: movePoint.x + Math.cos(angle) * textWidth,
          y: movePoint.y + Math.sin(angle) * textWidth
        }

        // Update data
        counter++
        if (counter > textValue.length - 1) {
          counter = 0
        }
      }
    }
  }

  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>
  )
}

Multi Line Connection

interface Point {
  x: number
  y: number
}

let isMouseDown = false
let movePoints: Point[] = [] // Mouse movement track point recording

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 = 3
        
        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 length = movePoints.length
      if (length) {
        // Normal line segment connection
        context2D.beginPath()
        context2D.moveTo(movePoints[length - 1].x, movePoints[length - 1].y)
        context2D.lineTo(clientX, clientY)
        context2D.stroke()

        /**
         * Linking of previous mouse points
         * Currently, connections are made at intervals of 5 points, and the number of connections is 3.
         */
        if (length % 5 === 0) {
          for (
            let i = movePoints.length - 5, count = 0;
            i >= 0 && count < 3;
            i = i - 5, count++
          ) {
            context2D.save()
            context2D.beginPath()
            context2D.lineWidth = 1
            context2D.moveTo(movePoints[length - 1].x, movePoints[length - 1].y)
            context2D.lineTo(movePoints[i].x, movePoints[i].y)
            context2D.stroke()
            context2D.restore()
          }
        }
      }
      movePoints.push({
        x: clientX,
        y: clientY
      })
    }
  }

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

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

Reticulate Brush

interface Point {
  x: number
  y: number
}

let isMouseDown = false
let movePoints: Point[] = [] // Mouse point recording

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 = 3
        
        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 length = movePoints.length
      if (length) {
        // Normal Drawing Connection
        context2D.beginPath()
        context2D.moveTo(movePoints[length - 1].x, movePoints[length - 1].y)
        context2D.lineTo(clientX, clientY)
        context2D.stroke()

        if (length % 2 === 0) {
          const limitDistance = 1000
          /**
           * If dx * dx + dy * dy < 1000, then the two points are considered to be close, and the line is quadratically connected.
           * limitDistance can be adjusted by yourself
           */
          for (let i = 0; i < length; i++) {
            const dx = movePoints[i].x - movePoints[length - 1].x
            const dy = movePoints[i].y - movePoints[length - 1].y
            const d = dx * dx + dy * dy
        
            if (d < limitDistance) {
              context2D.save()
              context2D.beginPath()
              context2D.lineWidth = 1
              context2D.moveTo(movePoints[length - 1].x, movePoints[length - 1].y)
              context2D.lineTo(movePoints[i].x, movePoints[i].y)
              context2D.stroke()
              context2D.restore()
            }
          }
        }
      }
      movePoints.push({
        x: clientX,
        y: clientY
      })
    }
  }

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

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

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!