npx create-react-app collaborate_client进入项目目录:要进入新创建的项目目录;
cd collaborate_client
npm install `socket.io`RoughJS:将rough.js库集成到协作板上,以实现绘图功能;
npm install --save roughjs
import React, { useLayoutEffect } from "react"; const WhiteBoard = () => { useLayoutEffect(() => { const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); // 堆代码 duidaima.com // Set canvas dimensions and initial styles canvas.width = window.innerWidth; canvas.height = window.innerHeight; ctx.strokeStyle = "black"; ctx.lineWidth = 5; // Implement drawing functionality here }, []); //implement event listeners for drawing interactions return ( <> <canvas id="canvas" width={window.innerWidth} height={window.innerHeight} ></canvas> </> ); }; export default WhiteBoard;在上面的代码中,我们导入了必要的依赖项,创建了一个 WhiteBoard 功能组件,并利用了 React 提供的 useLayoutEffect 钩子。在 useLayoutEffect 钩子内部,我们访问 canvas 元素及其2D渲染上下文,以配置其尺寸和初始样式。
import React, { useLayoutEffect } from "react"; import rough from "roughjs"; const WhiteBoard = () => { useLayoutEffect(() => { const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); // Initialize RoughJS instance const roughCanvas = rough.canvas(canvas); // Set canvas initial styles ctx.strokeStyle = "black"; ctx.lineWidth = 5; // Add event listeners for drawing interactions // Implement drawing functionality here }, []); return ( <> <canvas id="canvas" width={window.innerWidth} height={window.innerHeight} ></canvas> </> ); }; export default WhiteBoard;在更新的代码中,我们导入了 RoughJS 库,并使用 rough.canvas() 方法创建了一个实例,将其与我们的 canvas 元素关联起来。这个实例存储在 roughCanvas 中,它将允许我们应用 RoughJS 的基本图形和效果,从而可以在白板上绘制。使用 RoughJS,我们可以绘制各种形状、线条和阴影,无限可能。在本文中,我们将介绍如何在白板上绘制线条和矩形。您可以在此基础上进一步了解并添加其他RoughJS支持的形状和功能。
const [drawing, setDrawing] = useState(false); const [elements, setElements] = useState([]);处理鼠标按下事件:当用户按下鼠标按钮开始绘图时,我们将设置 drawing 状态为true,并创建一个新元素。在 handleMouseDown 函数中,我们利用初始 clientX 和 clientY 值来标记绘图的起点。当用户点击鼠标时,我们希望记录点击发生的位置,因为这将是他们即将绘制的线条的起点。
const handleMouseDown = (e) => { setDrawing(true); const { clientX, clientY } = e; // 堆代码 duidaima.com // Create a new line element with the same start and end points const element = createElement(clientX, clientY, clientX, clientY); setElements((prevState) => [...prevState, element]); };处理鼠标移动事件:在鼠标按钮仍按下的情况下,我们不断更新在 handleMouseDown 中创建的元素,以鼠标当前路径为用户在 canvas 上移动鼠标时的路径:
const handleMouseMove = (e) => { if (!drawing) return; const { clientX, clientY } = e; const index = elements.length - 1; const { x1, y1 } = elements[index]; // Update the end point of the element and create an element const updatedElement = createElement(x1, y1, clientX, clientY); const elementsCopy = [...elements]; elementsCopy[index] = updatedElement; setElements(elementsCopy); };创建我们的线:鼠标坐标将被发送到 createElement 函数,该函数利用 RoughJS 库生成元素的手绘表示。然后,该函数返回坐标和 RoughJS 元素,这些将被存储在我们的 elements 状态中。
const createElement = (x1, y1, x2, y2) => { // Use the RoughJS generator to create a rough element (line or rectangle) const roughElement = generator.line(x1, y1, x2, y2); // Return an object representing the element, including its coordinates and RoughJS representation return { x1, y1, x2, y2, roughElement }; };渲染我们的元素:使用我们的 useLayoutEffect 函数,在每次更新 elements 状态时,我们渲染存储在 state 中的元素。
useLayoutEffect(() => { // 通过ID获取画布元素 const canvas = document.getElementById("canvas"); // 获取画布的2D渲染上下文 const ctx = canvas.getContext("2d"); // 创建与画布元素相关联的RoughJS画布实例 const roughCanvas = rough.canvas(canvas); // 设置画布上下文的描边样式和线宽 ctx.strokeStyle = "black"; ctx.lineWidth = 5; // 清除整个画布以确保获得干净的绘图表面 ctx.clearRect(0, 0, canvas.width, canvas.height); // 如果有保存的元素需要渲染 if (elements && elements.length > 0) { // 遍历每个保存的元素 elements.forEach(({ roughElement }) => { // 使用RoughJS在画布上绘制元素 roughCanvas.draw(roughElement); }); } }, [elements]); // 此效果依赖于 'elements' 状态;当其更改时重新运行处理鼠标松开事件:当用户释放鼠标按钮时,我们将 drawing 状态设置为false,停止绘图过程;
const handleMouseUp = (e) => { setDrawing(false); };通过实施这些步骤,用户可以通过点击和拖动鼠标光标在 canvas 上绘制线条。这是具有在我们的 canvas 上绘制线条功能的 WhiteBoard 组件。
import React, { useState, useLayoutEffect } from "react"; import rough from "roughjs/bundled/rough.esm.js"; // 创建一个 RoughJS 生成器实例 const generator = rough.generator(); const WhiteBoard = () => { // 状态用于管理绘图元素和交互 const [elements, setElements] = useState([]); const [drawing, setDrawing] = useState(false); // useLayoutEffect: 负责渲染绘图元素 useLayoutEffect(() => { // 通过ID获取画布元素 const canvas = document.getElementById("canvas"); // 获取画布的2D渲染上下文 const ctx = canvas.getContext("2d"); // 创建与画布元素相关联的 RoughJS 画布实例 const roughCanvas = rough.canvas(canvas); // 为画布上下文设置描边样式和线宽 ctx.strokeStyle = "black"; ctx.lineWidth = 5; // 清除整个画布以确保一个干净的绘制表面 ctx.clearRect(0, 0, canvas.width, canvas.height); // 如果有保存的元素需要渲染 if (elements && elements.length > 0) { // 遍历每个保存的元素 elements.forEach(({ roughElement }) => { // 使用 RoughJS 在画布上绘制元素 roughCanvas.draw(roughElement); }); } }, [elements]); // 函数用于创建新的绘图元素 const createElement = (x1, y1, x2, y2) => { // 使用 RoughJS 生成器创建一个粗糙元素(线条或矩形) const roughElement = generator.line(x1, y1, x2, y2); // 返回一个表示元素的对象,包括其坐标和 RoughJS 表示 return { x1, y1, x2, y2, roughElement }; }; // 鼠标按下的事件处理程序 const handleMouseDown = (e) => { setDrawing(true); const { clientX, clientY } = e; // 当检测到鼠标按下时,创建一个新的绘图元素 const element = createElement(clientX, clientY, clientX, clientY); setElements((prevState) => [...prevState, element]); }; // 鼠标移动的事件处理程序 const handleMouseMove = (e) => { if (!drawing) return; const { clientX, clientY } = e; // 找到鼠标按下时创建的最后一个元素的索引 const index = elements.length - 1; const { x1, y1 } = elements[index]; // 更新元素的坐标以进行动态绘制 const updatedElement = createElement(x1, y1, clientX, clientY); const elementsCopy = [...elements]; elementsCopy[index] = updatedElement; setElements(elementsCopy); }; // 鼠标抬起的事件处理程序 const handleMouseUp = () => { setDrawing(false); }; // 返回 JSX 以渲染协作画布 return ( <> <canvas id="canvas" onMouseDown={handleMouseDown} onMouseUp={handleMouseUp} onMouseMove={handleMouseMove} width={window.innerWidth} height={window.innerHeight} ></canvas> </> ); }; export default WhiteBoard;让我们测试一下我们的应用程序:
const [tool, setTool] = useState('line');默认情况下,该工具设置为在线条上。现在我们可以更新我们的 createElement 函数以适应矩形。
const createElement = (x1, y1, x2, y2) => { let roughElement; // 使用 RoughJS 生成器创建粗糙元素(线条或矩形) if (tool === "line") { roughElement = generator.line(x1, y1, x2, y2); } else if (tool === "rect") { roughElement = generator.rectangle(x1, y1, x2 - x1, y2 - y1); } // 返回一个表示元素的对象,包括其坐标和 RoughJS 表示 return { x1, y1, x2, y2, roughElement }; };现在,我们需要添加按钮,让用户可以选择在我们的画布上使用哪种工具。
return ( <> <div className="d-flex col-md-2 justify-content-center gap-1"> <div className="d-flex gap-1 align-items-center"> <label htmlFor="line">Line</label> <input type="radio" id="line" name="tool" value="line" checked={tool === "line"} className="mt-1" onChange={(e) => setTool(e.target.value)} /> </div> <div className="d-flex gap-1 align-items-center"> <label htmlFor="rect">Rectangle</label> <input type="radio" name="tool" id="rect" checked={tool === "rect"} value="rect" className="mt-1" onChange={(e) => setTool(e.target.value)} /> </div> </div> <canvas id="canvas" onMouseDown={handleMouseDown} onMouseUp={handleMouseUp} onMouseMove={handleMouseMove} width={window.innerWidth} height={window.innerHeight} ></canvas> </> );现在,让我们测试我们的应用程序:
<input type="radio" id="selection" checked={tool === "selection"} onChange={() => setTool("selection")} /> <label htmlFor="selection">Drag n Drop</label>检测光标悬停在元素上:为了确定光标是否悬停在元素上,我们将实现一个名为 getElementAtPosition 的函数。该函数将在鼠标按下时判断光标是否在任何现有 elements 的边界内。
const distance = (a, b) => Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2)); const getElementAtPosition = (x, y) => { // 遍历 'elements' 数组中的每个元素 return elements.find((element) => { const { elementType, x1, y1, x2, y2 } = element; // 根据元素类型(线条或矩形),执行不同的检查 if (elementType === "rect") { // 检查光标位置(x, y)是否在矩形的边界内 const minX = Math.min(x1, x2); const maxX = Math.max(x1, x2); const minY = Math.min(y1, y2); const maxY = Math.max(y1, y2); return x >= minX && x <= maxX && y >= minY && y <= maxY; } else { // 使用数学偏移量检查光标是否足够接近线条 const a = { x: x1, y: y1 }; const b = { x: x2, y: y2 }; const c = { x, y }; const offset = distance(a, b) - (distance(a, c) + distance(b, c)); return Math.abs(offset) < 1; } }); };
getElementAtPosition 函数以当前光标坐标(x和y)作为参数。然后我们使用 .find() 方法遍历元素数组,该数组包含画布上的所有绘图元素。我们为数组中的每个元素检索 elementType 及其当前坐标。如果元素是一个矩形,我们计算最小和最大的 x 和 y 值来定义矩形的边界。然后我们检查光标的 x 坐标是否在矩形的 x 边界范围内,并且光标的 y 坐标是否在矩形的 y 边界范围内。
如果两个条件都为真,则光标位于矩形上方,因此我们的函数返回true。如果元素是一条线,我们计算光标坐标与由元素的 x1 、 y1 、 x2 和 y2 属性定义的线段之间的距离。然后我们将计算出的偏移量与一个小的阈值(在本例中为1)进行比较。如果偏移量的绝对值小于阈值,则认为光标位于线段附近,因此我们的函数返回true。 如果光标没有定位在任何现有元素上,该函数将返回false。
const handleMouseDown = (e) => { const { clientX, clientY } = e; // 检查当前工具是否为 "Selection" if (tool === "selection") { // 在点击位置查找元素 const element = getElementAtPosition(clientX, clientY); // 如果找到元素 if (element) { // 计算相对于元素左上角的偏移 const offsetX = clientX - element.x1; const offsetY = clientY - element.y1; // 存储选定的元素以及偏移量 setSelectedElement({ ...element, offsetX, offsetY }); // 将操作设置为 "moving",表示拖动正在进行中 setAction("moving"); } } else { // 如果工具不是 "Selection",执行绘图的代码 // ...(用于绘制的代码) } };更新元素坐标:在 handleMouseMove 函数中,当用户处于“移动”状态(即拖动元素)时,我们根据鼠标光标的位置和初始偏移量计算元素的新位置。然后使用 updateElement 函数更新元素的坐标。
const handleMouseMove = (e) => { const { clientX, clientY } = e; // 检查当前工具是否为 "Selection" if (tool === "selection") { // 根据鼠标是否在元素上确定光标样式 e.target.style.cursor = getElementAtPosition(clientX, clientY, elements) ? "move" : "default"; } // 检查当前操作 if (action === "drawing") { // ...(用于绘制的代码) } else if (action === "moving") { // 如果处于 "moving" 操作中(拖动元素) const { id, x1, x2, y1, y2, elementType, offsetX, offsetY } = selectedElement; const width = x2 - x1; const height = y2 - y1; // 计算被拖动元素的新位置 const newX = clientX - offsetX; const newY = clientY - offsetY; // 更新元素的坐标以执行拖动操作 const updatedElement = createElement( id, newX, newY, newX + width, newY + height, elementType ); const elementsCopy = [...elements]; elementsCopy[id] = updatedElement; setElements(elementsCopy); } };按照这些步骤,我们为我们的画布添加了动态拖放功能。用户现在可以轻松地与现有元素进行交互,将它们在画布上移动。
npm install express cors socket.ioExpress :一个受欢迎且灵活的Node.js框架,简化了构建强大的Web应用程序和API的过程。它提供了中间件和路由功能,非常适合创建服务器端应用程序。
touch server.js然后我们将导入依赖项并为 Express 设置配置:
const express = require('express'); const cors = require('cors'); const app = express(); const PORT = process.env.PORT || 5000;
const { createServer } = require("http"); const { Server } = require("socket.io"); const httpServer = createServer(); const io = new Server(httpServer, { cors: { origin: "http://localhost:3000", // Specify your front-end origin AccessControlAllowOrigin: "http://localhost:3000", allowedHeaders: ["Access-Control-Allow-Origin"], credentials: true, }, }); httpServer.listen(PORT, () => { console.log(`Server is listening on port ${PORT}`); });在这个设置中,我们创建了一个 Express 应用程序,并设置了 CORS 配置,以允许客户端(在端口3000上运行)和服务器之间的通信。 socket.io 库已集成到 httpServer 实例中,实现实时通信。
const [socket, setSocket] = useState(null); // useEffect 钩子用于建立和管理套接字连接 useEffect(() => { // 定义服务器 URL const server = "http://localhost:5000"; // 套接字连接的配置选项 const connectionOptions = { "force new connection": true, reconnectionAttempts: "Infinity", timeout: 10000, transports: ["websocket"], }; // 建立新的套接字连接 const newSocket = io(server, connectionOptions); setSocket(newSocket); // 用于成功连接的事件监听器 newSocket.on("connect", () => { console.log("已连接到 newSocket.io 服务器!"); }); // 用于从服务器接收服务元素的事件监听器 newSocket.on("servedElements", (elementsCopy) => { setElements(elementsCopy.elements); }); // 在组件卸载时清理套接字连接 return () => { newSocket.disconnect(); }; }, []); // 空的依赖数组确保该效果仅在组件挂载时运行一次我们将利用 socket.io 的事件驱动架构,采用其 on 和 emit 机制,以促进客户端和服务器之间的无缝数据传输。在客户端方面,我们将增强 updateElement 功能,使其在每次元素更新时将数据传输到服务器。
const updateElement = (id, x1, y1, x2, y2, tool) => { const UpdatedElement = createElement(id, x1, y1, x2, y2, tool); const elementsCopy = [...elements]; elementsCopy[id] = UpdatedElement; setElements(elementsCopy); socket.emit("elements", elementsCopy); };随后,我们的服务器将把接收到的数据发送给网络中的其他连接客户端。这确保了所有参与者之间的实时同步和协作。
let connections = []; let elements; socket.on("elements", (data) => { elements = data; connections.forEach((con) => { if (con.id !== socket.id) { con.emit("servedElements", { elements }); } }); });当数据传递给其他客户端时,我们将更新接收到的状态,从而导致重新渲染,从而在画布上绘制更新后的元素
new socket.on("servedElements", (elementsCopy) => { setElements(elementsCopy.elements); });完成此操作后,每当一个客户端进行更新时,连接到我们服务器的所有其他客户端都会收到更新。现在,让我们测试我们的应用程序: