-
Notifications
You must be signed in to change notification settings - Fork 85
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
loadPaths() is laggy for data coming in from onUpdate() via socket-io (Collaborative canvas) #14
Comments
Hey @AllanMwirigi, Thank you for the kind words and detailed description. I would suggest you take the low-level Canvas class and move the path handling to Socker server instead. You can copy the path handlers from ReactSketchCanvas class and put it in the socket server then sync it up. Here is a very simple example that I wrote long ago. Hope it still works. Server"use strict";
var _immutable = require("immutable");
var express = require("express");
var http = require("http");
var socketIO = require("socket.io");
// our localhost port
var port = 8000;
var app = express();
app.get('/', function (req, res) {
return res.send('Hello World!');
});
// our server instance
var server = http.createServer(app);
// This creates our socket using the instance of the server
var io = socketIO(server);
var drawMode = true;
var isDrawing = false;
var currentPaths = new _immutable.List();
var message = "";
var canvasColor = "white";
var strokeColor = "black";
var strokeWidth = 4;
var eraserWidth = 8;
// This is what the socket.io syntax is like, we will work this later
io.on("connection", function (socket) {
console.log("User connected");
io.emit("receive_message", message);
io.emit("receive_paths", {
currentPaths: currentPaths,
isDrawing: isDrawing
});
socket.on("disconnect", function () {
console.log("user disconnected");
});
socket.on("send_message", function (msg) {
message = msg;
io.emit("receive_message", message);
});
// Pointer down event
socket.on("sketchPointerDown", function (point) {
isDrawing = true;
currentPaths = currentPaths.push(new _immutable.Map({
drawMode: drawMode,
strokeColor: drawMode ? strokeColor : canvasColor,
strokeWidth: drawMode ? strokeWidth : eraserWidth,
paths: new _immutable.List([point])
}));
// console.log(currentPaths.size);
io.emit("receive_paths", {
currentPaths: currentPaths,
isDrawing: isDrawing
});
});
// Pointer move event
socket.on("sketchPointerMove", function (point) {
if (!isDrawing) return;
currentPaths = currentPaths.updateIn([currentPaths.size - 1], function (pathMap) {
return pathMap.updateIn(["paths"], function (list) {
return list.push(point);
});
});
io.emit("receive_paths", {
currentPaths: currentPaths,
isDrawing: isDrawing
});
});
// Pointer up event
socket.on("sketchPointerUp", function () {
isDrawing = false;
});
});
server.listen(port, '0.0.0.0', function () {
return console.log("Listening on port " + port);
}); Clientclass Sketch extends Component {
constructor(props) {
super(props);
this.socket = socketIOClient("http://127.0.0.1:8000");
this.state = {
paths: [],
isDrawing: false
};
this.socket.on("receive_paths", data => {
this.updatePaths(data);
});
this.updatePaths = this.updatePaths.bind(this);
this.handlePointerDown = this.handlePointerDown.bind(this);
this.handlePointerMove = this.handlePointerMove.bind(this);
this.handlePointerUp = this.handlePointerUp.bind(this);
}
handlePointerDown = point => {
this.socket.emit("sketchPointerDown", point);
};
handlePointerMove = point => {
this.socket.emit("sketchPointerMove", point);
};
handlePointerUp = () => {
this.socket.emit("sketchPointerUp");
};
updatePaths = ({ currentPaths, isDrawing }) => {
this.setState({
paths: fromJS(currentPaths),
isDrawing
});
};
render() {
return (
<Canvas
allowOnlyPointerType="all"
paths={this.state.paths}
isDrawing={this.state.isDrawing}
onPointerDown={this.handlePointerDown}
onPointerMove={this.handlePointerMove}
onPointerUp={this.handlePointerUp}
/>
);
}
} |
Thanks for the quick reply. |
Hello Here is my code. The only major change I made was to wrap the path data inside an object called workspaces in order to support rooms. In the workspaces, the roomName is the key and the path data is the value. I use this to identify the room and send data to the right room. Server"use strict";
const _immutable = require("immutable");
const logger = require('./utils/winston');
const workspaces = {};
exports.initSync = (server) => {
const socketio = require('socket.io')(server, {
cors: {
// NOTE!!!: in case domain is changed, ensure to replace/update these; local and prod domains
origin: ["http://localhost:3000"],
// if using socket.io v3, then these two are needed; had to downgrade to v2.3 because ngx-socket-io client in Angular didn't seem to be comaptible, was giving 400 errors
methods: ["GET", "POST"],
// credentials: true
}
});
// sockets for real time data
socketio.on('connection', (socket) => {
socket.on('join-room', ({ roomName, userName }) => {
// each user in a workspace will join a room identified by the room name
// create a new entry in workspaces if none exists
if (workspaces[roomName] == null) {
workspaces[roomName] = {
drawMode: true,
isDrawing: false,
currentPaths: new _immutable.List(),
canvasColor: "white",
strokeColor: "red",
strokeWidth: 4,
eraserWidth: 20,
}
}
socket.join(roomName);
logger.debug(`socket ${socket.id} joined room ${roomName}`);
socket.to(roomName).emit('join-room', userName);
socketio.to(roomName).emit('whiteboard-paths', {
currentPaths: workspaces[roomName].currentPaths,
isDrawing: workspaces[roomName].isDrawing
});
});
// socket.on('whiteboard-paths', (data) => {
// // send drawn changes to the other user in the workspace room
// const { roomName, type, drawUpdates } = data;
// // socket.to(roomName).emit('whiteboard', { type, drawUpdates });
// socket.to(roomName).emit('whiteboard-paths', { currentPaths, isDrawing });
// });
socket.on('chat-msg', (data) => {
const { roomName, txt, senderName } = data;
socket.to(roomName).emit('chat-msg', { txt, senderName });
});
// Pointer down event
socket.on("sketchPointerDown", function ({ roomName, point }) {
logger.debug(`pointerDown ${JSON.stringify(point)}`)
if (workspaces[roomName] == null) {
workspaces[roomName] = {
drawMode: true,
isDrawing: false,
currentPaths: new _immutable.List(),
canvasColor: "white",
strokeColor: "red",
strokeWidth: 4,
eraserWidth: 20,
}
}
workspaces[roomName].isDrawing = true;
const { drawMode, strokeColor, canvasColor, strokeWidth, eraserWidth } = workspaces[roomName];
workspaces[roomName].currentPaths = workspaces[roomName].currentPaths.push(new _immutable.Map({
drawMode: drawMode,
strokeColor: drawMode ? strokeColor : canvasColor,
strokeWidth: drawMode ? strokeWidth : eraserWidth,
paths: new _immutable.List([point])
}));
socketio.to(roomName).emit('whiteboard-paths', {
currentPaths: workspaces[roomName].currentPaths,
isDrawing: workspaces[roomName].isDrawing
});
});
// Pointer move event
socket.on("sketchPointerMove", function ({ roomName, point }) {
logger.debug(`pointerMove ${JSON.stringify(point)}`)
if (workspaces[roomName] == null) {
workspaces[roomName] = {
drawMode: true,
isDrawing: false,
currentPaths: new _immutable.List(),
canvasColor: "white",
strokeColor: "red",
strokeWidth: 4,
eraserWidth: 20,
}
}
if (!workspaces[roomName].isDrawing) return;
workspaces[roomName].currentPaths = workspaces[roomName].currentPaths.updateIn([workspaces[roomName].currentPaths.size - 1], function (pathMap) {
return pathMap.updateIn(["paths"], function (list) {
return list.push(point);
});
});
socketio.to(roomName).emit('whiteboard-paths', {
currentPaths: workspaces[roomName].currentPaths,
isDrawing: workspaces[roomName].isDrawing
});
});
// Pointer up event
socket.on("sketchPointerUp", function ({roomName}) {
if (workspaces[roomName] == null) {
workspaces[roomName] = {
drawMode: true,
isDrawing: false,
currentPaths: new _immutable.List(),
canvasColor: "white",
strokeColor: "red",
strokeWidth: 4,
eraserWidth: 20,
}
}
workspaces[roomName].isDrawing = false;
});
});
} Clientimport { produce } from "immer";
import React from "react";
import { Canvas, CanvasPath, Point } from "react-sketch-canvas";
import ReactTooltip from "react-tooltip";
import { fromJS } from 'immutable';
import { getsocketIoInstance } from '../utils/socketio-client';
/* Default settings */
const defaultProps = {
width: "100%",
height: "100%",
className: "",
canvasColor: "white",
strokeColor: "red",
background: "",
strokeWidth: 4,
eraserWidth: 20,
allowOnlyPointerType: "all",
style: {
border: "0.0625rem solid #9c9c9c",
borderRadius: "0.25rem",
},
// eslint-disable-next-line @typescript-eslint/no-empty-function
onUpdate: (_: CanvasPath[]): void => { },
withTimestamp: false,
};
/* Props validation */
export type ReactSketchCanvasProps = {
width: string;
height: string;
className: string;
strokeColor: string;
canvasColor: string;
background: string;
strokeWidth: number;
eraserWidth: number;
allowOnlyPointerType: string;
onUpdate: (updatedPaths: CanvasPath[]) => void;
style: React.CSSProperties;
withTimestamp: boolean;
};
export type ReactSketchCanvasStates = {
drawMode: boolean;
isDrawing: boolean;
resetStack: CanvasPath[];
undoStack: CanvasPath[];
currentPaths: CanvasPath[];
eraseMode: boolean;
};
export class Whiteboard extends React.Component<
ReactSketchCanvasProps,
ReactSketchCanvasStates
> {
static defaultProps = defaultProps;
svgCanvas: React.RefObject<Canvas>;
initialState = {
drawMode: true,
isDrawing: false,
// eslint-disable-next-line react/no-unused-state
resetStack: [],
undoStack: [],
currentPaths: [],
eraseMode: false,
};
roomName: string | null;
socketIo: any;
constructor(props: ReactSketchCanvasProps) {
super(props);
this.state = this.initialState;
this.handlePointerDown = this.handlePointerDown.bind(this);
this.handlePointerMove = this.handlePointerMove.bind(this);
this.handlePointerUp = this.handlePointerUp.bind(this);
this.eraseMode = this.eraseMode.bind(this);
this.clearCanvas = this.clearCanvas.bind(this);
this.undo = this.undo.bind(this);
this.redo = this.redo.bind(this);
this.resetCanvas = this.resetCanvas.bind(this);
this.liftPathsUp = this.liftPathsUp.bind(this);
this.svgCanvas = React.createRef();
this.roomName = sessionStorage.getItem('roomName');
this.socketIo = getsocketIoInstance(this.roomName, 'Whiteboard');
}
componentDidMount() {
this.socketIo.on('whiteboard-paths', ({ currentPaths, isDrawing }: { currentPaths: CanvasPath[]; isDrawing: boolean; }) => {
console.log('whiteboard-paths', { currentPaths, isDrawing })
// update paths
this.setState({
currentPaths: fromJS(currentPaths),
isDrawing
});
});
}
// updatePaths = (changes: { currentPaths: CanvasPath[]; isDrawing: boolean; }) => {
// const { currentPaths, isDrawing } = changes;
// this.setState({
// paths: fromJS(currentPaths),
// isDrawing
// });
// };
resetCanvas(): void {
this.setState(this.initialState);
}
liftPathsUp(): void {
const { currentPaths } = this.state;
const { onUpdate } = this.props;
onUpdate(currentPaths);
}
/* Mouse Handlers - Mouse down, move and up */
handlePointerDown(point: Point): void {
this.socketIo.emit("sketchPointerDown", { roomName: this.roomName, point });
}
handlePointerMove(point: Point): void {
this.socketIo.emit("sketchPointerMove", { roomName: this.roomName, point });
}
handlePointerUp(): void {
this.socketIo.emit("sketchPointerUp", { roomName: this.roomName });
}
/* Mouse Handlers ends */
/* Canvas operations */
eraseMode(erase: boolean): void {
this.setState(
produce((draft: ReactSketchCanvasStates) => {
draft.drawMode = !erase;
}),
this.liftPathsUp
);
}
toggleEraseMode = () => {
this.eraseMode(!this.state.eraseMode);
this.setState({ eraseMode: !this.state.eraseMode })
}
clearCanvas(): void {
this.setState(
produce((draft: ReactSketchCanvasStates) => {
draft.resetStack = draft.currentPaths;
draft.currentPaths = [];
}),
this.liftPathsUp
);
// this.socketIo.emit("sketch-clear", { roomName: this.roomName });
}
undo(): void {
const { resetStack } = this.state;
// If there was a last reset then
if (resetStack.length !== 0) {
this.setState(
produce((draft: ReactSketchCanvasStates) => {
draft.currentPaths = draft.resetStack;
draft.resetStack = [];
}),
() => {
const { currentPaths } = this.state;
const { onUpdate } = this.props;
onUpdate(currentPaths);
}
);
// this.socketIo.emit("sketch-undo", { roomName: this.roomName });
return;
}
this.setState(
produce((draft: ReactSketchCanvasStates) => {
const lastSketchPath = draft.currentPaths.pop();
if (lastSketchPath) {
draft.undoStack.push(lastSketchPath);
}
}),
this.liftPathsUp
);
// this.socketIo.emit("sketch-undo", { roomName: this.roomName });
}
redo(): void {
const { undoStack } = this.state;
// Nothing to Redo
if (undoStack.length === 0) return;
this.setState(
produce((draft: ReactSketchCanvasStates) => {
const lastUndoPath = draft.undoStack.pop();
if (lastUndoPath) {
draft.currentPaths.push(lastUndoPath);
}
}),
this.liftPathsUp
);
// this.socketIo.emit("sketch-redo", { roomName: this.roomName });
}
/* Finally!!! Render method */
render(): JSX.Element {
const {
width,
height,
className,
canvasColor,
background,
style,
allowOnlyPointerType,
} = this.props;
const { currentPaths, isDrawing } = this.state;
return (
<div className="whiteboard">
<h4>Whiteboard</h4>
<ReactTooltip id="whtbrd-tltp" place="top" type="info" effect="float" />
<div className="whiteboard-icons">
<i className="fas fa-undo" data-tip='Undo' onClick={this.undo} data-for="whtbrd-tltp"></i>
<i className="fas fa-redo" data-tip='Redo' onClick={this.redo} data-for="whtbrd-tltp"></i>
<i className="fas fa-eraser" data-tip={this.state.eraseMode ? 'Stop Erase' : 'Erase'}
onClick={this.toggleEraseMode} data-for="whtbrd-tltp"></i>
<i className="fas fa-broom" data-tip='Clear' onClick={this.clearCanvas} data-for="whtbrd-tltp"></i>
</div>
<Canvas
ref={this.svgCanvas}
width={width}
height={height}
className={className}
canvasColor={canvasColor}
background={background}
allowOnlyPointerType={allowOnlyPointerType}
style={style}
paths={currentPaths}
isDrawing={isDrawing}
onPointerDown={this.handlePointerDown}
onPointerMove={this.handlePointerMove}
onPointerUp={this.handlePointerUp}
/>
</div>
);
}
} |
Hello, again Server"use strict";
const logger = require('./utils/winston');
const workspaces = {};
exports.initSync = (server) => {
const socketio = require('socket.io')(server, {
cors: {
// NOTE!!!: in case domain is changed, ensure to replace/update these; local and prod domains
origin: ["http://localhost:3000"],
// if using socket.io v3, then these two are needed; had to downgrade to v2.3 because ngx-socket-io client in Angular didn't seem to be comaptible, was giving 400 errors
methods: ["GET", "POST"],
// credentials: true
}
});
// sockets for real time data
socketio.on('connection', (socket) => {
socket.on('join-room', ({ roomName, userName }) => {
// each user in a workspace will join a room identified by the room name
// create a new entry in workspaces if none exists
if (workspaces[roomName] == null) {
workspaces[roomName] = {
drawMode: true,
isDrawing: false,
// currentPaths: new _immutable.List(),
currentPaths: [],
canvasColor: "white",
strokeColor: "red",
strokeWidth: 4,
eraserWidth: 20,
}
}
socket.join(roomName);
logger.debug(`socket ${socket.id} joined room ${roomName}`);
socket.to(roomName).emit('join-room', userName);
socketio.to(roomName).emit('whiteboard-paths', {
currentPaths: workspaces[roomName].currentPaths,
isDrawing: workspaces[roomName].isDrawing
});
});
socket.on('chat-msg', (data) => {
const { roomName, txt, senderName } = data;
socket.to(roomName).emit('chat-msg', { txt, senderName });
});
// Pointer down event
socket.on("sketchPointerDown", function ({ roomName, point }) {
// logger.debug(`pointerDown ${JSON.stringify(point)}`)
if (workspaces[roomName] == null) {
workspaces[roomName] = {
drawMode: true,
isDrawing: false,
currentPaths: [],
canvasColor: "white",
strokeColor: "red",
strokeWidth: 4,
eraserWidth: 20,
}
}
workspaces[roomName].isDrawing = true;
const { drawMode, strokeColor, canvasColor, strokeWidth, eraserWidth } = workspaces[roomName];
const cp = workspaces[roomName].currentPaths.slice();
cp.push({
drawMode: drawMode,
strokeColor: drawMode ? strokeColor : canvasColor,
strokeWidth: drawMode ? strokeWidth : eraserWidth,
paths: [point]
});
workspaces[roomName].currentPaths = cp;
socketio.to(roomName).emit('whiteboard-paths', {
currentPaths: workspaces[roomName].currentPaths,
isDrawing: workspaces[roomName].isDrawing
});
});
// Pointer move event
socket.on("sketchPointerMove", function ({ roomName, point }) {
// logger.debug(`pointerMove ${JSON.stringify(point)}`)
if (workspaces[roomName] == null) {
workspaces[roomName] = {
drawMode: true,
isDrawing: false,
currentPaths: [],
canvasColor: "white",
strokeColor: "red",
strokeWidth: 4,
eraserWidth: 20,
}
}
if (!workspaces[roomName].isDrawing) return;
const cp = workspaces[roomName].currentPaths.slice();
cp[workspaces[roomName].currentPaths.length - 1].paths.push(point);
workspaces[roomName].currentPaths = cp;
socketio.to(roomName).emit('whiteboard-paths', {
currentPaths: workspaces[roomName].currentPaths,
isDrawing: workspaces[roomName].isDrawing
});
});
// Pointer up event
socket.on("sketchPointerUp", function ({roomName}) {
if (workspaces[roomName] == null) {
workspaces[roomName] = {
drawMode: true,
isDrawing: false,
currentPaths: [],
canvasColor: "white",
strokeColor: "red",
strokeWidth: 4,
eraserWidth: 20,
}
}
workspaces[roomName].isDrawing = false;
});
});
} |
You don't have to use Immutable. You can do by writing pure functions in your code or try immer library. |
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
Describe the bug
Hello. Awesome work on the canvas!!
I am trying to sync two whiteboards with each other. But the two keep getting laggy till it's barely usable
To Reproduce
When something is drawn on the first whiteboard, I get the CanvasPath[ ] from onUpdate() and send to the second whiteboard via socketio.
On the second whiteboard, socketio captures the sent CanvasPath[ ] and I use loadPath() to display it on the screen.
It works okay for the first few seconds and what is drawn on one end reflects quickly on the other.
Then gets keeps getting laggy to the point where there is a huge delay. There is even a lag when drawing on the original whiteboard, leave alone reflecting on the other one.
I also noticed that loadPaths() was trigerring onUpdate(), so I disabled socketio sending the paths until data is loaded
Expected behavior
loadPaths() should not cause a lag
Screenshots
If applicable, add screenshots to help explain your problem.
Desktop (please complete the following information):
My code
`
import React,{ useState, useEffect } from "react";
import { ReactSketchCanvas } from "react-sketch-canvas";
import ReactTooltip from 'react-tooltip';
import { getsocketIoInstance } from '../utils/socketio-client';
export default class Whiteboard extends React.Component {
constructor(props) {
super(props);
this.styles = {
border: "0.0625rem solid #9c9c9c",
borderRadius: "0.25rem",
};
this.canvas = React.createRef();
this.state = {
eraseMode: false
}
this.pauseSync = false;
this.WhiteBoardMsgType = {
canvas_draw: 1,
canvas_undo: 2,
canvas_redo: 3,
canvas_clear: 4,
}
this.roomName = sessionStorage.getItem('roomName');
this.socketIo = getsocketIoInstance(this.roomName, 'Whiteboard');
}
componentDidMount() {
this.socketIo.on('whiteboard', (changes) => {
const { type, drawUpdates } = changes;
if (type === this.WhiteBoardMsgType.canvas_draw) {
this.pauseSync = true;
this.canvas.current.loadPaths(drawUpdates);
this.pauseSync = false;
// setTimeout(() => {
// this.pauseSync = false;
// }, 50);
}
});
}
whiteBoardUpdated = (drawUpdates) => {
if (!this.pauseSync) {
const changes = { roomName: this.roomName, type: this.WhiteBoardMsgType.canvas_draw, drawUpdates }
this.socketIo.emit('whiteboard', changes);
}
// console.log('pause sync', this.pauseSync);
}
toggleEraseMode = () => {
this.canvas.current.eraseMode(!this.state.eraseMode);
this.setState({ eraseMode: !this.state.eraseMode })
}
undoCanvas = () => {
this.canvas.current.undo();
// no need to send this change as they are already captured in the drawUpdates
// const changes = { roomName: this.roomName, type: this.WhiteBoardMsgType.canvas_undo }
// this.socketIo.emit('whiteboard', changes);
}
redoCanvas = () => {
this.canvas.current.redo();
// no need to send this change as they are already captured in the drawUpdates
// const changes = { roomName: this.roomName, type: this.WhiteBoardMsgType.canvas_redo }
// this.socketIo.emit('whiteboard', changes);
}
clearCanvas = () => {
this.canvas.current.clearCanvas();
// no need to send this change as they are already captured in the drawUpdates
// const changes = { roomName: this.roomName, type: this.WhiteBoardMsgType.canvas_clear }
// this.socketIo.emit('whiteboard', changes);
}
render() {
return (
Whiteboard
<i className="fas fa-eraser" data-tip={this.state.eraseMode ? 'Stop Erase': 'Erase'}
onClick={this.toggleEraseMode}>
<i className="fas fa-broom"data-tip='Clear' onClick={this.clearCanvas}>
<ReactSketchCanvas
ref={this.canvas}
style={this.styles}
// width="600"
// height="400"
strokeWidth={4}
strokeColor="red"
eraserWidth={20}
onUpdate={this.whiteBoardUpdated}
/>
}
}
`
The text was updated successfully, but these errors were encountered: