画板探秘系列:创意画笔第一期

前言

我目前在维护一款功能强大的开源创意画板。这个画板集成了多种创意画笔,可以让用户体验到全新的绘画效果。无论是在移动端还是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>)
}

总结

感谢你的阅读。以上就是本文的全部内容,希望这篇文章对你有所帮助,欢迎点赞和收藏。如果有任何问题,欢迎在评论区进行讨论

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/819612.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【SQL】DISTINCT GROUP BY

找到所有办公室里的所有角色&#xff08;包含没有雇员的&#xff09;,并做唯一输出(DISTINCT) 用DISTINCT : SELECT DISTINCT B.Building_name,E.Role FROM Buildings B LEFT JOIN Employees EON B.Building_name E.Building需要找到的结果&#xff1a;所有办公室名字&#…

iOS framework 相关知识

苹果官方目前不允许开发者使用动态库&#xff0c;所以下面只说明相关静态库使用知识&#xff0c;目前只做简单的记录&#xff0c;后续会做完整的整理 如何在同一个WorkSpace里面创建一个主工程和多个子framework 首先创键一个工程&#xff0c;然后创建一个workspace&#xf…

线程通信-java

线程通信 当多个线程操作共享多资源时&#xff0c;线程间通过某种方式相互告知自己的状态&#xff0c;以相互协调&#xff0c; 并避免无效的资源争夺。 线程通信的常见模型&#xff08;生产者与消费者模型&#xff09; 生产者线程负责生产数据 消费者线程负责消费生产者生…

表单自定义系统源码:自主创建表单 带完整的安装代码包以及搭建教程

在当今信息化社会&#xff0c;表单作为一种常见的数据收集工具&#xff0c;广泛应用于各类网站和系统中。然而&#xff0c;传统的表单系统往往功能单一&#xff0c;缺乏灵活性&#xff0c;难以满足用户多样化的需求。下面&#xff0c;小编给大家分享一款表单自定义系统源码&…

YOLOv8改进 添加大核卷积序列注意力机制LSK

一、Large Separable Kernel Attention论文 论文地址:2309.01439.pdf (arxiv.org) 二、Large Separable Kernel Attention注意力结构 LSK通过使用大型可分离卷积核来提升注意力机制的效果。在传统的注意力机制中,常用的是小型卷积核,如1x1卷积,来计算注意力权重和特征表示…

微信小程序英文版:实现一键切换中英双语版(已组件化)

已经重新优化代码做成了组件&#xff0c;需要可自取&#xff1a;https://github.com/CrystalCAI11/wechat-language-compoment 所有操作都打包在组件里不需要在额外的地方添加代码&#xff0c;直接在你需要的页面里导入组件&#xff0c;再在对应页面的onLoad()里set文本就行了。…

【嵌入式】嵌入式开发中常见的面试题(持续更新中)

&#x1f9d1; 作者简介&#xff1a;阿里巴巴嵌入式技术专家&#xff0c;深耕嵌入式人工智能领域&#xff0c;具备多年的嵌入式硬件产品研发管理经验。 &#x1f4d2; 博客介绍&#xff1a;分享嵌入式开发领域的相关知识、经验、思考和感悟,欢迎关注。提供嵌入式方向的学习指导…

【位运算】Leetcode 消失的两个数字

题目解析 面试题 17.19. 消失的两个数字 算法讲解 我们将这两个数组异或在一起&#xff0c;最后的结果就是a ^ b(缺失的两个数字)的结果&#xff0c;这两个缺失的数字一定是不相同的&#xff0c;所以我们就寻找他们第一个比特位是1的那个位置&#xff0c;异或的原理是&#xf…

半导体存储电路知识点总结

目录 一、SR锁存器 1.SR锁存器的概念 2.作用 二、电平触发器&#xff08;Flip-Flop&#xff09; 1.时钟信号 2.电平触发的触发器电路结构 3.带异步置位复位的电平触发器 三、边沿触发器 1.特点 2.两个D触发器组成的边沿触发D触发器 3.CMOS边沿触发D触发器的典型电路 …

FMEA分析

目录 1、FMEA的核心目的 2、FMEA的种类 3、FMEA的实施步骤 4、FMEA的SOD等级 5、FMEA的例子 FMEA&#xff08;Failure Modes and Effects Analysis&#xff0c;失效模式与影响分析&#xff09;是一种预防性的可靠性设计分析&#xff0c;用来确定潜在失效模式及其原因。它主…

【Altium Designer 20 笔记】PCB线宽与过孔尺寸

电源线&#xff1a;40mil1A&#xff08;一般翻倍给&#xff09;,地线比电源线粗一点即可&#xff1b;信号线&#xff1a;10-15mil 一、线宽 市电的火线和零线&#xff1a;80-100mil12V /24V 20mil~60mil 5V 20-30mil 3V 20-30mil GND 越宽越好20-30mil普通信号线 10mil-15mil…

比亚迪老总王传福:雨天走访县级市场,生活方式却如此简单

近期&#xff0c; 世界汽车销售排名前十的老板亲自下到一线市场调研&#xff0c;这听起来像是个奇迹&#xff0c;但这就是我们今天要说的主角——比亚迪老总王传福的故事。他不仅亲自去现场调研&#xff0c;还时常挤地铁、吃盒饭、坐经济舱&#xff0c;这和那些高高在上的企业领…

flink network buffer

Flink 的网络协议栈是组成 flink-runtime 模块的核心组件之一&#xff0c;是每个 Flink 作业的核心。它连接所有 TaskManager 的各个子任务(Subtask)&#xff0c;因此&#xff0c;对于 Flink 作业的性能包括吞吐与延迟都至关重要。与 TaskManager 和 JobManager 之间通过基于 A…

YOLOv8最新改进系列:融合DySample超轻量动态上采样算子,低延迟、高性能,目前最新上采样方法!!!遥遥领先!

YOLOv8最新改进系列&#xff1a;融合DySample超轻量动态上采样算子&#xff0c;低延迟、高性能&#xff0c;目前最新上采样方法&#xff01;&#xff01;&#xff01;遥遥领先&#xff01; DySample超轻量动态上采样算子全文戳这&#xff01;here! 详细的改进教程以及源码&am…

在EC2上面安装Skywalking9.7.0

问题 前几天在k8s集群安装了skywalking&#xff0c;说什么这种方式不行&#xff0c;客户要求单独在一台linux机器安装skywalking。现在我们来解决这个问题。 步骤 # 移动到/opt目录 cd /opt # 下载apm安装包 sudo wget https://dlcdn.apache.org/skywalking/9.7.0/apache-sk…

超越GPT-4V!马斯克发布Grok-1.5 With Vision

在 Grok-1 开源后不到一个月&#xff0c;xAI 的首个多模态模型就问世了。Grok-1.5V是XAI的第一代多模态模型&#xff0c;除了其强大的文本处理能力之外&#xff0c;Grok现在还能够处理包括文档、图表、图形、屏幕截图和照片在内的各种视觉信息。相信Grok-1.5V将很快提供给现有的…

ViT——nlp和cv进行了统一,使多模态成为可能

题目:AN IMAGE IS WORTH 16X16 WORDS: TRANSFORMERS FOR IMAGE RECOGNITION AT SCALE 1.概述之前的transformer在cv中应用,大部分是将CNN模型中部分替换成transformer block(整体网络结构不变)或者用transformer将不同网络连接起来,而本文提出:一个针对图像patch的纯的t…

WebShell简介

WebShell简介 1、WebShell分类 • JSP类型 • ASP类型 • PHP类型 2、WebShell用途 • 站长工具 • 持续远程控制 • 权限提升 • 极强隐蔽性 3、WebShell检测方法 • 基于流量的 WebShell 检测 • 基于文件的 WebShell 检测 • 基于日志的 WebShell 检测 WebShe…

看到这12这个登录页,我感觉自己的很多登录白设计啦。

登录页是B端系统的脸面&#xff0c;它是B端系统的入口&#xff0c;是用户与系统之间的第一道门槛。登录页的设计直接影响用户对系统的第一印象&#xff0c;因此登录页的设计应该简洁、清晰、易于使用&#xff0c;并且能够符合用户的需求和期望。

深度学习框架

深度学习框架 1 引言 在当今技术加速发展的时代&#xff0c;深度学习已经成为了人工智能领域内最为引人注目的子领域之一。其在图像识别、自然语言处理、自动驾驶等多个行业中的成功应用&#xff0c;已经证明了深度学习在解决复杂问题方面的巨大潜力。然而&#xff0c;深度学习…