前言
我目前在维护一款功能强大的开源创意画板。这个画板集成了多种创意画笔,可以让用户体验到全新的绘画效果。无论是在移动端还是PC端,都能享受到较好的交互体验和效果展示。并且此项目拥有许多强大的辅助绘画功能,包括但不限于前进后退、复制删除、上传下载、多画板和多图层等等。详细功能我就不一一罗列了,期待你的探索。
Link: https://songlh.top/paint-board/
Github: https://github.com/LHRUN/paint-board 欢迎Star⭐️
在项目的逐渐迭代中,我计划撰写一些文章,一方面是为了记录技术细节,这是我一直以来的习惯。另一方面则是为了推广一下,期望得到你的使用和反馈,当然如果能点个 Star 就是对我最大的支持。
我准备分3篇文章讲解创意画笔的实现, 本篇文章是第一篇, 所有的实现源码我都会上传到我的 Github 上.
实现源码Demo
彩虹画笔
- 彩虹画笔是会在绘制中不断变换颜色, 效果如下:
- 这个效果相比于常规画笔只是多了一个颜色的转换, 我们知道常规画笔的实现是通过一个个线段连接而成, 无论你是曲线还是直线. 所以我们为了实现彩虹效果, 是需要改变每个线段的颜色, 改变线段颜色是通过修改
strokeStyle
属性 - 然后改变
strokeStyle
颜色, 就不能简单的用常规的颜色表达, 我们需要知道一个知识HSL
HSL
是一种颜色表达方式, 它通过圆柱坐标系来描述颜色, 分为色相(H), 饱和度(S), 亮度(L)- 色相(H): 表示颜色在色环上的位置
- 饱和度(S): 它表示颜色的纯度或者说是灰度的程度。饱和度为 100% 表示完全饱和的颜色,而 0% 则表示灰度色。
- 亮度(L): 它表示颜色的亮度。调整亮度可以改变颜色的明暗程度。
- 具体概念可以看 MDN
- HSL在线展示网站: https://mothereffinghsl.com/
- 而我们只需要不断的调整
HSL
表达中的hue
, 就可以达到颜色不断变换的效果
let hue = 0 // 记录当前的色相
let isMouseDown = false // 是否点击鼠标
let movePoint: { x: number, y: number } | null = null // 记录鼠标位置
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) {
/**
* 在 0 到 360 的范围内, 逐步增加1, 但是当大于等于 360 时, 再回归为0
*/
hue = hue < 360 ? hue + 1 : 0
context2D.beginPath()
// 通过 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>
)
}
多形状画笔
- 多形状画笔是会随着鼠标移动, 在移动路径上随机生成点位进行形状绘制, 效果如下
- 实现方法, 通过每次鼠标移动的坐标, 在这个坐标的周围范围内随机生成几个点位, 然后在这些点位通过
new Path2D()
进行生成图形路径, 然后绘制
let isMouseDown = false
// 音乐符号形状路径
const musicPath = '***'
/**
* 矩形内生成随机点位
*/
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 = '#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
}
// 路径绘制
const path = new Path2D(musicPath);
context2D.beginPath();
context2D.save();
context2D.translate(x, y);
// 形状随机缩放
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>
)
}
素材画笔
- 素材画笔效果如下
- 素材画笔首先是需要一张透明的素材图, 这个图作为底图, 如果你用蜡笔材质的图片, 就会有蜡笔效果, 如果你用磨砂材质的图片, 就会有磨砂效果
- 然后
strokeStyle
属性可以接收一个CanvasPattern
对象, 详情可看 MDN - 我们可以新建一个
canvas
, 然后对这个canvas
绘制一张素材图片, 再绘制一个你需要的颜色, 最后通过这个canvas
创建一个pattern
赋值到strokeStyle
就可以出现素材画笔的效果
let isMouseDown = false
let movePoint: { x: number, y: number } | null = null
// 加载所需素材图
const materialImage = new Promise<HTMLImageElement>((resolve) => {
const image = new Image()
image.src = '素材图地址'
image.onload = () => {
resolve(image)
}
})
/**
* 获取 pattern 对象
* @param color 需要生成的颜色
*/
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
// 绘制一个矩形作为底色
context.fillRect(0, 0, 100, 100)
const image = await materialImage
// 绘制素材图
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
// 获取 pattern 素材
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) {
// 画笔绘制
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>
)
}
像素画笔
- 像素画笔效果如下
- 像素画笔是通过鼠标移动, 在鼠标移动路径上, 随机根据点位进行矩形绘制, 多个矩形组合起来就有一种类似像素点的效果
let isMouseDown = false
const drawWidth = 15 // 像素画笔大小
const step = 5 // 每个像素点大小
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
/**
* 遍历当前像素画笔大小, 根据随机数判断是否绘制
*/
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>
)
}
总结
感谢你的阅读。以上就是本文的全部内容,希望这篇文章对你有所帮助,欢迎点赞和收藏。如果有任何问题,欢迎在评论区进行讨论