news 2026/9/25 11:21:04

50天50个小项目 (React19 + Tailwindcss V4) ✨ | DrawingApp(画板组件)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
50天50个小项目 (React19 + Tailwindcss V4) ✨ | DrawingApp(画板组件)

📅 我们继续 50 个小项目挑战!—— DrawingApp 组件

仓库地址:https://gitee.com/hhm-hhm/50days50projects.git

构建一个简单的在线画板应用。用户可以自由绘制图形、调节画笔粗细、选择颜色,并支持一键清空画布。

🌀 组件目标

  • 创建一个固定尺寸的画布区域
  • 支持鼠标点击拖动进行绘画
  • 提供按钮控制画笔粗细(+ / -)
  • 使用原生<input type="color">选择画笔颜色
  • 提供“清空”按钮重置画布内容
  • 使用 TailwindCSS 快速构建现代 UI 界面

🔧 DrawingApp.tsx组件实现

import React, { useRef, useEffect, useState } from 'react' const DrawingApp: React.FC = () => { // Refs const canvasRef = useRef<HTMLCanvasElement>(null) const isDrawingRef = useRef(false) // 使用 ref 避免 draw 闭包问题 const lastXRef = useRef(0) const lastYRef = useRef(0) const ctxRef = useRef<CanvasRenderingContext2D | null>(null) // State const [brushSize, setBrushSize] = useState<number>(5) const [brushColor, setBrushColor] = useState<string>('#000000') // 初始化画布 useEffect(() => { const canvas = canvasRef.current if (!canvas) return // 设置画布尺寸为显示尺寸(避免模糊) const dpr = window.devicePixelRatio || 1 const rect = canvas.getBoundingClientRect() canvas.width = rect.width * dpr canvas.height = rect.height * dpr const ctx = canvas.getContext('2d') if (!ctx) return // 缩放上下文以适配高清屏 ctx.scale(dpr, dpr) ctx.lineCap = 'round' ctx.lineJoin = 'round' ctxRef.current = ctx }, []) // 开始绘制(仅左键) const startDrawing = (e: React.MouseEvent<HTMLCanvasElement>) => { if (e.button !== 0) return // 只响应左键 const canvas = canvasRef.current if (!canvas) return const rect = canvas.getBoundingClientRect() const x = e.clientX - rect.left const y = e.clientY - rect.top lastXRef.current = x lastYRef.current = y isDrawingRef.current = true } // 绘制中 const draw = (e: React.MouseEvent<HTMLCanvasElement>) => { if (!isDrawingRef.current || !ctxRef.current) return const canvas = canvasRef.current if (!canvas) return const rect = canvas.getBoundingClientRect() const x = e.clientX - rect.left const y = e.clientY - rect.top const ctx = ctxRef.current ctx.beginPath() ctx.moveTo(lastXRef.current, lastYRef.current) ctx.lineTo(x, y) ctx.strokeStyle = brushColor ctx.lineWidth = brushSize ctx.stroke() lastXRef.current = x lastYRef.current = y } // 停止绘制 const stopDrawing = () => { isDrawingRef.current = false } // 控制画笔大小 const increaseBrushSize = () => { setBrushSize((prev) => Math.min(prev + 1, 50)) } const decreaseBrushSize = () => { setBrushSize((prev) => Math.max(prev - 1, 1)) } // 清空画布 const clearCanvas = () => { const canvas = canvasRef.current const ctx = ctxRef.current if (!canvas || !ctx) return ctx.clearRect( 0, 0, canvas.width / (window.devicePixelRatio || 1), canvas.height / (window.devicePixelRatio || 1) ) } return ( <div className="flex min-h-screen items-center justify-center bg-gray-900"> <div className="flex flex-col items-center"> {/* 🎨 画板区域 */} <canvas ref={canvasRef} className="aspect-square w-[800px] border-2 border-gray-300 bg-white" onMouseDown={startDrawing} onMouseMove={draw} onMouseUp={stopDrawing} onMouseLeave={stopDrawing} onContextMenu={(e) => e.preventDefault()} // 禁用右键菜单 /> {/* 🛠️ 工具栏 */} <div className="mt-4 flex w-[800px] items-center justify-between rounded-lg bg-gray-800 p-3"> {/* 粗细调节 */} <div className="flex items-center"> <button onClick={decreaseBrushSize} className="rounded p-2 text-white hover:bg-gray-700" disabled={brushSize <= 1}> - </button> <span className="mx-3 text-white">{brushSize}</span> <button onClick={increaseBrushSize} className="rounded p-2 text-white hover:bg-gray-700" disabled={brushSize >= 50}> + </button> </div> {/* 🎨 颜色选择 */} <input type="color" value={brushColor} onChange={(e) => setBrushColor(e.target.value)} className="h-10 w-10 cursor-pointer appearance-none rounded border-0 bg-transparent" /> {/* 清空画布 */} <button onClick={clearCanvas} className="rounded bg-red-600 p-2 text-white hover:bg-red-700"> 清空 </button> </div> </div> <div className="fixed right-20 bottom-5 text-2xl text-red-500">CSDN@Hao_Harrision</div> </div> ) } export default DrawingApp

🔍 关键技术说明

1.使用useRef管理可变状态

  • isDrawing,lastX,lastY使用ref而非state,避免draw函数因闭包捕获旧值。
  • ctx也用ref缓存,避免重复获取。

2.高 DPI 屏幕适配(防模糊)

  • 获取devicePixelRatio并放大 canvas 尺寸;
  • 同时缩放绘图上下文(ctx.scale(dpr, dpr));
  • 清空时需除以dpr得到逻辑尺寸。

3.坐标计算

  • 使用getBoundingClientRect()获取 canvas 位置;
  • clientX/Y - rect.left/top得到相对于 canvas 的坐标。

4.事件处理

  • onMouseDown/onMouseMove等使用 React 事件系统;
  • onContextMenu阻止默认右键菜单。

5.无障碍与 UX

  • 按钮添加disabled状态(当画笔已达最小/最大);
  • 颜色选择器移除浏览器默认样式:appearance-none+border-0。

💡 可选增强建议

功能实现方式
移动端支持添加onTouchStart/onTouchMove等事件
撤销功能保存 canvas 快照到栈中
导出图片使用canvas.toDataURL()
自定义背景在clearCanvas中填充背景色或图案

🎨 TailwindCSS 样式重点讲解

🎯 TailwindCSS 样式说明
类名作用
min-h-screen设置最小高度为视口高度
items-center,justify-centerFlexbox 居中对齐布局
bg-gray-900设置深色背景
aspect-square保持画布为正方形比例
w-[800px]固定宽度为 800px
border-2,border-gray-300边框样式
bg-white画布背景色
rounded-lg,p-3工具栏圆角与内边距
hover:bg-gray-700按钮悬停变色
ext-white白色文字
cursor-pointer鼠标悬停变为手型
h-10,w-10设置颜色选择器大小

🦌 路由组件 + 常量定义

router/index.tsx中children数组中添加子路由

{ path: '/', element: <App />, children: [ ... { path: '/DrawingApp', lazy: () => import('@/projects/DrawingApp.tsx').then((mod) => ({ Component: mod.default, })), }, ], },

constants/index.tsx 添加组件预览常量

import demo22Img from '@/assets/pic-demo/demo-22.png' 省略部分.... export const projectList: ProjectItem[] = [ 省略部分.... { id: 22, title: 'DrawingApp', image: demo22Img, link: 'DrawingApp', },

🚀 小结

你可以进一步扩展此组件的功能,例如:

  • ✅ 支持保存画布内容为图片(canvas.toDataURL())
  • ✅ 添加撤销/重做功能(记录历史快照)
  • ✅ 支持触控设备(如 iPad 或触摸屏)
  • ✅ 封装为独立组件(支持 props 传入默认颜色或大小)

📅 明日预告: 我们将完成KineticLoader组件,一个很有意思的旋转加载动画。🚀


原文链接:https://blog.csdn.net/qq_44808710/article/details/149150719

每天造一个轮子,码力暴涨不是梦!🚀

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/1 23:51:44

C020基于博途西门子1200PLC鸡饲料生产线控制系统仿真

C020基于博途西门子1200PLC鸡饲料生产线控制系统仿真C020鸡饲料生产线S71200HMI主电路图外部接线图IO分配表资料包含&#xff1a; 1.程序和HMI仿真工程&#xff08;博图V16及以上版本可以打开&#xff09; 2.PLC端口定义IO分配表1份 3.PLC外部接线图CAD版本和PDF版本各1份 4.主…

作者头像 李华
网站建设 2026/9/24 21:39:54

互联网大厂Java面试:从Spring Boot到微服务架构的技术探讨

互联网大厂Java面试&#xff1a;从Spring Boot到微服务架构的技术探讨 场景介绍 在某知名互联网大厂的办公室&#xff0c;一位严肃的面试官正在面试一位名叫“超好吃”的求职者。面试以音视频场景为背景&#xff0c;旨在考察求职者在Java及相关技术栈上的理解与应用能力。 面…

作者头像 李华
网站建设 2026/9/23 7:39:29

等待信号节点-–-behaviac

原文 等待信号&#xff08;WaitforSignal&#xff09;节点模拟了等待某个条件的“阻塞”过程。 等待信号节点返回Running&#xff0c;直到它上面附加的条件是true的时候&#xff1a; 如果有子节点&#xff0c;则执行其子节点&#xff0c;并当子节点结束时&#xff0c;返回该…

作者头像 李华
网站建设 2026/9/24 19:13:52

python django flask嗨玩-旅游线路社区交流商城网站_mvyi06ne--论文

文章目录 系统截图项目技术简介可行性分析主要运用技术介绍核心代码参考示例结论源码lw获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&#xff01; 系统截图 python django flask嗨玩-旅游线路社区交流商城网站_mvyi06ne–论文 项目技术简介 Pyth…

作者头像 李华
网站建设 2026/9/23 21:52:45

写在创作第 730 天:一些关于学习、技术与自我认知的记录

目录 从入坑网络安全开始&#xff1a;写作是另一种学习监督 关于“学不会”的现实&#xff1a;接受边界&#xff0c;尊重选择 关于圈子与交流&#xff1a;你出彩了&#xff0c;人自然会来 比赛、学习与现实&#xff1a;看到进步本身就很快乐 以赛促学&#xff1a;主动给自…

作者头像 李华
网站建设 2026/9/23 16:05:36

提示工程架构师必备,实用工具箱大放送

提示工程架构师必备&#xff1a;从0到1搭建你的「提示工具链」 一、引言&#xff1a;你调的不是提示&#xff0c;是「工具链缺失的焦虑」 1. 一个扎心的问题&#xff1a;你还在「手动调提示」吗&#xff1f; 上周和一位做AI产品的朋友聊天&#xff0c;他拍着桌子吐槽&#x…

作者头像 李华