Last active
March 20, 2024 11:04
-
-
Save zinedkaloc/61074ef5d6546dc74a85798021f51223 to your computer and use it in GitHub Desktop.
Real-Time Collaborative Drawing App with React and Agnost
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useState, useEffect } from "react"; | |
import { createClient } from "@agnost/client"; | |
import styled from "styled-components"; | |
import * as fal from "@fal-ai/serverless-client"; | |
fal.config({ | |
// Can also be auto-configured using environment variables: | |
// Either a single FAL_KEY or a combination of FAL_KEY_ID and FAL_KEY_SECRET | |
credentials: | |
"181f3044-5f0b.......65775f5bbcceb424", | |
}); | |
const seed = Math.floor(Math.random() * 100000); | |
const baseArgs = { | |
sync_mode: true, | |
strength: 0.99, | |
seed, | |
}; | |
const Container = styled.div` | |
max-width: 800px; | |
margin: 0 auto; | |
padding: 20px; | |
display: flex; | |
justify-content: space-between; | |
`; | |
const CanvasContainer = styled.div` | |
position: relative; | |
`; | |
const Canvas = styled.canvas` | |
border: 1px solid #ccc; | |
cursor: crosshair; | |
`; | |
const ColorPalette = styled.div` | |
display: flex; | |
margin-bottom: 20px; | |
`; | |
const ColorButton = styled.button` | |
width: 30px; | |
height: 30px; | |
border: none; | |
margin-right: 5px; | |
cursor: pointer; | |
background-color: ${({ color }) => color}; | |
`; | |
const BrushSizeSlider = styled.input` | |
width: 100px; | |
margin-right: 10px; | |
`; | |
const SaveButton = styled.button` | |
padding: 10px; | |
background-color: #007bff; | |
color: #fff; | |
border: none; | |
cursor: pointer; | |
`; | |
const Form = styled.form` | |
margin-bottom: 20px; | |
`; | |
const LoadButton = styled.button` | |
padding: 10px; | |
background-color: #28a745; | |
color: #fff; | |
border: none; | |
cursor: pointer; | |
`; | |
const UserList = styled.ul` | |
list-style: none; | |
padding: 0; | |
`; | |
const UserListItem = styled.li` | |
margin-bottom: 5px; | |
`; | |
const baseUrl = "https://cloudflex.app/env-p3....k7g"; | |
const clientKey = "ak-xp0hbz.....oipc9x3b5as7"; | |
let options = { | |
signInRedirect: "http://localhost:3000/auth-redirect", | |
realtime: { | |
autoJoinChannels: false, | |
bufferMessages: true, | |
echoMessages: true, | |
reconnectionDelay: 2000, | |
timeout: 30000, | |
}, | |
}; | |
const agnost = createClient(baseUrl, clientKey, options); | |
function App() { | |
const [userName, setUserName] = useState(""); | |
const [isDrawing, setIsDrawing] = useState(false); | |
const [prevPosition, setPrevPosition] = useState(null); | |
const [cursorPosition, setCursorPosition] = useState({ x: 0, y: 0 }); | |
const [color, setColor] = useState("#000000"); | |
const [brushSize, setBrushSize] = useState(2); | |
const [users, setUsers] = useState([]); | |
const [joinedRoom, setJoinedRoom] = useState(false); | |
const [generatedImage, setGeneratedImage] = useState(null); // State to hold the generated image | |
useEffect(() => { | |
// Open connection | |
agnost.realtime.open(); | |
// Listen for drawing events from other users | |
agnost.realtime.on("draw", handleDrawingEvent); | |
// Listen for user join and leave events | |
agnost.realtime.onJoin(handleUserJoin); | |
agnost.realtime.onLeave(handleUserLeave); | |
// Cleanup function | |
return () => { | |
agnost.realtime.close(); | |
agnost.realtime.off("draw", handleDrawingEvent); | |
agnost.realtime.offAny(handleUserJoin); | |
agnost.realtime.onLeave(handleUserLeave); | |
}; | |
}, []); | |
const handleUserJoin = async () => { | |
const membersData = await agnost.realtime.getMembers("drawing-room"); | |
const memberIds = membersData.data.map((member) => member.data.username); | |
console.log("Members:", membersData.data); | |
setUsers(memberIds); | |
}; | |
const handleUserLeave = async () => { | |
const membersData = await agnost.realtime.getMembers("drawing-room"); | |
const memberIds = membersData.data.map((member) => member.id); | |
console.log("Members:", membersData.data); | |
setUsers(memberIds); | |
}; | |
const handleDrawingEvent = (payload) => { | |
if ( | |
payload.message && | |
payload.message.position && | |
payload.message.prevPosition | |
) { | |
const { position, prevPosition, color, brushSize } = payload.message; | |
const { x, y } = position; | |
const { x: prevX, y: prevY } = prevPosition; | |
drawLine(prevX, prevY, x, y, color, brushSize); | |
} | |
}; | |
const startDrawing = (event) => { | |
const canvas = event.target; | |
const rect = canvas.getBoundingClientRect(); | |
const x = event.clientX - rect.left; | |
const y = event.clientY - rect.top; | |
setIsDrawing(true); | |
setPrevPosition({ x, y }); | |
}; | |
const endDrawing = async () => { | |
setIsDrawing(false); | |
setPrevPosition(null); | |
// Generate image when the drawing ends | |
await handleGenerateImage(); | |
}; | |
const draw = (event) => { | |
event.preventDefault(); // Prevent default browser actions | |
if (!isDrawing) return; | |
const canvas = event.target; | |
const rect = canvas.getBoundingClientRect(); | |
const x = event.pageX - rect.left; | |
const y = event.pageY - rect.top; | |
const { x: prevX, y: prevY } = prevPosition; | |
drawLine(prevX, prevY, x, y, color, brushSize); | |
setPrevPosition({ x, y }); | |
// Broadcast the drawing event to other users | |
agnost.realtime.broadcast("draw", { | |
position: { x, y }, | |
prevPosition, | |
color, | |
brushSize, | |
}); | |
}; | |
const handleMouseMove = (event) => { | |
const canvas = event.target; | |
const rect = canvas.getBoundingClientRect(); | |
const x = event.pageX - rect.left; | |
const y = event.pageY - rect.top; | |
setCursorPosition({ x, y }); | |
}; | |
const drawLine = (prevX, prevY, x, y, color, brushSize) => { | |
const canvas = document.getElementById("canvas"); | |
const ctx = canvas.getContext("2d"); | |
ctx.beginPath(); | |
ctx.moveTo(prevX, prevY); | |
ctx.lineTo(x, y); | |
ctx.strokeStyle = color; | |
ctx.lineWidth = brushSize; | |
ctx.lineCap = "round"; | |
ctx.stroke(); | |
}; | |
const handleNameChange = (event) => { | |
setUserName(event.target.value); | |
}; | |
const handleJoinRoom = async (event) => { | |
event.preventDefault(); | |
// Join the room with the given user name | |
agnost.realtime.join("drawing-room"); | |
await agnost.realtime.updateProfile({ username: userName }); | |
setJoinedRoom(true); // Set joinedRoom to true after joining the room | |
}; | |
const handleLeaveRoom = async () => { | |
// Leave the room | |
await agnost.realtime.leave("drawing-room"); | |
setJoinedRoom(false); // Set joinedRoom to false after leaving the room | |
}; | |
const handleColorChange = (color) => { | |
setColor(color); | |
}; | |
const handleBrushSizeChange = (event) => { | |
setBrushSize(parseInt(event.target.value)); | |
}; | |
const handleSaveDrawing = () => { | |
const canvas = document.getElementById("canvas"); | |
const dataURL = canvas.toDataURL("image/png"); | |
// Implement saving the drawing dataURL to the server or local storage | |
console.log("Drawing saved:", dataURL); | |
}; | |
const handleLoadDrawing = () => { | |
// Implement loading a saved drawing from the server or local storage | |
console.log("Loading drawing..."); | |
}; | |
const handleGenerateImage = async () => { | |
// Extract drawing data from canvas | |
const canvas = document.getElementById("canvas"); | |
const dataURL = canvas.toDataURL("image/png"); | |
// Communicate with Falcon AI to generate the image | |
const response = await fal.realtime.connect( | |
"110602490-sdxl-turbo-realtime", | |
{ | |
connectionKey: "realtime-nextjs-app", | |
onResult: (result) => { | |
if (result.error) { | |
console.error("Error generating image:", result.error); | |
} else { | |
setGeneratedImage(result.images[0].url); | |
} | |
}, | |
} | |
); | |
// Send the drawing data to Falcon AI for image generation | |
response.send({ | |
image_url: dataURL, // Sending the drawing data as prompt for Falcon AI | |
...baseArgs, | |
prompt: "A drawing generated by a user", | |
// You can include additional parameters here as needed | |
}); | |
}; | |
return ( | |
<Container> | |
<div> | |
<h2>Real-Time Drawing</h2> | |
{/* Conditionally render based on whether the user has joined the room or not */} | |
{!joinedRoom && ( | |
<Form onSubmit={handleJoinRoom}> | |
<label>Enter your name:</label> | |
<input type="text" value={userName} onChange={handleNameChange} /> | |
<button type="submit">Join Room</button> | |
</Form> | |
)} | |
{/* Conditionally render based on whether the user has joined the room or not */} | |
{joinedRoom && ( | |
<Form onSubmit={handleLeaveRoom}> | |
<button type="submit">Leave Room</button> | |
</Form> | |
)} | |
<ColorPalette> | |
<ColorButton | |
color="#000000" | |
onClick={() => handleColorChange("#000000")} | |
/> | |
<ColorButton | |
color="#ff0000" | |
onClick={() => handleColorChange("#ff0000")} | |
/> | |
<ColorButton | |
color="#00ff00" | |
onClick={() => handleColorChange("#00ff00")} | |
/> | |
<ColorButton | |
color="#0000ff" | |
onClick={() => handleColorChange("#0000ff")} | |
/> | |
</ColorPalette> | |
<BrushSizeSlider | |
type="range" | |
min="1" | |
max="20" | |
value={brushSize} | |
onChange={handleBrushSizeChange} | |
/> | |
<SaveButton onClick={handleSaveDrawing}>Save Drawing</SaveButton> | |
<LoadButton onClick={handleLoadDrawing}>Load Drawing</LoadButton> | |
<UserList> | |
{users.map((user, index) => ( | |
<UserListItem key={index}>{user}</UserListItem> | |
))} | |
</UserList> | |
</div> | |
<CanvasContainer> | |
<Canvas | |
id="canvas" | |
width={800} | |
height={600} | |
onMouseDown={startDrawing} | |
onMouseUp={endDrawing} | |
onMouseOut={endDrawing} | |
onMouseMove={(event) => { | |
draw(event); | |
}} | |
/> | |
</CanvasContainer> | |
{/* Display the generated image if available */} | |
{generatedImage && ( | |
<div> | |
<h2>Generated Image</h2> | |
<img | |
src={generatedImage} | |
width={400} | |
height={400} | |
alt="Generated Image" | |
/> | |
</div> | |
)} | |
</Container> | |
); | |
} | |
export default App; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment