前言
我目前在维护一款功能强大的开源创意画板。这个画板集成了多种创意画笔,可以让用户体验到全新的绘画效果。无论是在移动端还是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 = 10setContext2D(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 } = eventif (movePoint) {/*** 在 0 到 360 的范围内, 逐步增加1, 但是当大于等于 360 时, 再回归为0*/hue = hue < 360 ? hue + 1 : 0context2D.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 = falsemovePoint = null}return (<div><canvasref={canvasRef}onMouseDown={onMouseDown}onMouseMove={onMouseMove}onMouseUp={onMouseUp}/></div>)
}
多形状画笔
- 多形状画笔是会随着鼠标移动, 在移动路径上随机生成点位进行形状绘制, 效果如下
- 实现方法, 通过每次鼠标移动的坐标, 在这个坐标的周围范围内随机生成几个点位, 然后在这些点位通过
new Path2D()
进行生成图形路径, 然后绘制
let isMouseDown = false// 音乐符号形状路径
const musicPath = '***'/*** 矩形内生成随机点位*/
const generateRandomCoordinates = (centerX: number, // 矩形中心点 XcenterY: number, // 矩形中心点 Ysize: number, // 矩形大小count: number // 生成数量
) => {const halfSize = size / 2const 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 } = eventconst 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.5context2D.scale(scale, scale);context2D.fill(path);context2D.restore();}const onMouseUp = () => {if (!canvasRef?.current || !context2D) {return}isMouseDown = falsemoveDate = 0}return (<div><canvasref={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 CanvasRenderingContext2Dcanvas.width = 100canvas.height = 100context.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 } = eventif (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 = falsemovePoint = null}return (<div><canvasref={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><canvasref={canvasRef}onMouseDown={onMouseDown}onMouseMove={onMouseMove}onMouseUp={onMouseUp}/></div>)
}
总结
感谢你的阅读。以上就是本文的全部内容,希望这篇文章对你有所帮助,欢迎点赞和收藏。如果有任何问题,欢迎在评论区进行讨论