前言
我目前在维护一款功能强大的开源创意画板。这个画板集成了多种创意画笔,可以让用户体验到全新的绘画效果。无论是在移动端还是PC端,都能享受到较好的交互体验和效果展示。并且此项目拥有许多强大的辅助绘画功能,包括但不限于前进后退、复制删除、上传下载、多画板和多图层等等。详细功能我就不一一罗列了,期待你的探索。
Link: https://songlh.top/paint-board/
Github: https://github.com/LHRUN/paint-board 欢迎Star⭐️
在项目的逐渐迭代中,我计划撰写一些文章,一方面是为了记录技术细节,这是我一直以来的习惯。另一方面则是为了推广一下,期望得到你的使用和反馈,当然如果能点个 Star 就是对我最大的支持。
我准备分3篇文章讲解创意画笔的实现, 本篇文章是第二篇, 所有的实现源码我都会上传到我的 Github 上.
多色画笔
- 多色画笔效果如下
- 多色画笔的实现类似于素材画笔, 都是通过
strokeStyle
接收一个CanvasPattern
对象 - 我们可以新建一个
canvas
, 然后对这个canvas
进行你想要的效果进行绘制, 最后通过这个canvas
创建一个pattern
赋值到strokeStyle
就可以出现多色画笔的效果
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 // 每个颜色的宽度
/**
* 获取多色画笔 pattern
* @param colors 颜色数组, 需要绘制的颜色
*/
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')
}
/**
* 横排效果绘制
*/
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
// 根据生成的 pattern 赋值到 strokeStyle
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>
)
}
文字画笔
- 文字画笔是会跟着鼠标移动进行文字绘制, 效果如下
- 文字画笔绘制拆解分为三步,
-
- 首先根据两次移动的坐标得出位移距离, 然后通过
measureText
得出文字的宽度, 如果距离大于文字宽度说明是可以绘制的
- 首先根据两次移动的坐标得出位移距离, 然后通过
-
- 然后通过两个点的向量, 根据
Math.atan2
得出角度, 然后根据这个角度绘制当前的某个文字
- 然后通过两个点的向量, 根据
-
- 最后更新轨迹坐标, 同时绘制文字坐标调整为下一个, 如果绘制完就重新开始
-
interface Point {
x: number
y: number
}
let isMouseDown = false
let movePoint: Point = { x: 0, y: 0 }
let counter = 0 // 当前需要绘制文字的下标
const textValue = 'PaintBoard' // 绘制文字内容
const minFontSize = 5 // 文字绘制最小限制
/**
* 根据勾股定理得出距离
*/
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
// 得出两点距离
const distance = getDistance(movePoint, { x: clientX, y: clientY })
const fontSize = minFontSize + distance
const letter = textValue[counter]
context2D.font = `${fontSize}px Georgia`
// 得出文字宽度
const textWidth = context2D.measureText(letter).width
if (distance > textWidth) {
// 计算当前移动角度
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();
// 更新文字绘制后位置
movePoint = {
x: movePoint.x + Math.cos(angle) * textWidth,
y: movePoint.y + Math.sin(angle) * textWidth
}
// 更新所绘文字下标
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>
)
}
多线连接
- 多线连接效果如下:
- 多线连接是在正常的绘制连接中, 会对以前的轨迹点进行二次连接, 然后通过调整需要连接的点间隔或者连接数量, 可以达到不同的效果
interface Point {
x: number
y: number
}
let isMouseDown = false
let movePoints: 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 = 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) {
// 正常线段连接
context2D.beginPath()
context2D.moveTo(movePoints[length - 1].x, movePoints[length - 1].y)
context2D.lineTo(clientX, clientY)
context2D.stroke()
/**
* 对以往的轨迹点进行连接
* 目前是每间隔5个点进行连接, 连接数量为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>
)
}
网状画笔
- 网状画笔效果如下
- 网状画笔是在正常的绘制过程中, 会对以往的轨迹点进行遍历, 如果满足一定条件就会判断视为相近, 然后对相近的点进行二次连接, 多个连接线就会达到网状的效果
interface Point {
x: number
y: number
}
let isMouseDown = false
let movePoints: 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 = 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) {
// 正常绘制连接
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
/**
* 如果满足 dx * dx + dy * dy < 1000 就视为两点相近, 进行线段二次连接
* 这个 limitDistance 可以自行调整
*/
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>
)
}
总结
感谢你的阅读。以上就是本文的全部内容,希望这篇文章对你有所帮助,欢迎点赞和收藏。如果有任何问题,欢迎在评论区进行讨论