DEV Community

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

Posted on

Exploring the Canvas Series: Creative Brushes Part 1

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

Github Source Code Demo

Rainbow Brush

  • The rainbow brush changes colour as it is being drawn, the effect is as follows.

rainbow

  • This effect is just an extra colour shift compared to a normal brush, which as we know is implemented by connecting line segments one by one, whether they are curves or straight lines. So in order to achieve the rainbow effect, we need to change the colour of each line segment, which is done by modifying strokeStyle.
  • Then to change the strokeStyle colour, we can't simply express it as a regular colour, we need to know about HSL.
    • HSL is a colour expression that describes colours by means of a cylindrical coordinate system, divided into Hue (H), Saturation (S), Luminance (L).
    • Hue (H): Indicates the position of the colour on the colour ring.
    • Saturation (S): It indicates the purity or degree of greyness of a colour. A saturation of 100% indicates a fully saturated colour, while 0% indicates a greyish colour.
    • Luminance (L): It indicates the brightness of the colour. Adjusting the Luminance changes the lightness or darkness of the colour.
    • MDN Detailed description
    • HSL Online Showcase Website: https://mothereffinghsl.com/
  • Instead, we just need to keep adjusting the hue in the HSL expression to achieve the effect of constantly changing colours.
let hue = 0 // Record the current hue
let isMouseDown = false
let movePoint: { x: number, y: number } | null = null // Record mouse point

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) {
        /**
         * Gradually increases by 1 in the range 0 to 360, but resets to 0 when greater than or equal to 360.
         */
        hue = hue < 360 ? hue + 1 : 0
        context2D.beginPath()
        // Change colours via 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Multi Shape Brush

  • Multi-shape brush will move with the mouse, in the path of the move to generate random points for shape drawing, the effect is as follows

shape

  • Implementation method, through each mouse movement coordinates, in the coordinates of the surrounding range of a few randomly generated points, and then in these points through the new Path2D() for the generation of graphical path, and then draw the
let isMouseDown = false

// Music Symbol Shape Path
const musicPath = '***'

/**
 * 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 = '#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
    }
    // Path creation
    const path = new Path2D(musicPath);
    context2D.beginPath();

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

    // Shape random scaling
    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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Material Brush

  • The effect of the material brush is as follows

material

  • The first thing you need is a transparent picture of the material, this picture is used as a base, if you use a picture made of crayon material, it will have a crayon effect, if you use a picture made of frosted material, it will have a frosted effect.
  • The strokeStyle property can then take a CanvasPattern object, MDN
  • We can create a new canvas, then draw an image on this canvas, then draw a colour you want, and finally create a pattern from this canvas and assign it to strokeStyle to get the effect of the material's brush.
let isMouseDown = false
let movePoint: { x: number, y: number } | null = null

// Load the required material image
const materialImage = new Promise<HTMLImageElement>((resolve) => {
  const image = new Image()
  image.src = 'Material image URL'
  image.onload = () => {
    resolve(image)
  }
})

/**
 * Get pattern object
 * @param color background colour
 */
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

  // Draw a rectangle as the background colour
  context.fillRect(0, 0, 100, 100)
  const image = await materialImage

  // Drawing of material
  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
        // get pattern material
        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) {
        // Brush Drawing
        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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Pixel Brush

  • The pixel brush effect is as follows

pixels

  • Pixel Paintbrush is through the mouse to move, in the path of the mouse to move, randomly point according to the position of the rectangle drawing, multiple rectangles combined to have a similar effect to the pixel dot
let isMouseDown = false
const drawWidth = 15 // Pixel Brush Size
const step = 5 // Size of each pixel point

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

      /**
       * Iterate over the current pixel brush size, and determine whether to draw based on a random number.
       */
      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>
  )
}
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)