From 301972cf8d05c1ace32a13757e0f020359e2b4c2 Mon Sep 17 00:00:00 2001 From: sujatagunale Date: Mon, 29 Jan 2024 12:35:29 +0530 Subject: [PATCH 1/6] add comments --- app/App.tsx | 249 +++++++++++++++++++++++- app/Room.tsx | 17 ++ app/page.tsx | 5 + components/LeftSidebar.tsx | 1 + components/Live.tsx | 52 ++++- components/Navbar.tsx | 3 + components/RightSidebar.tsx | 62 +++--- components/ShapesMenu.tsx | 1 + components/comments/CommentsOverlay.tsx | 32 +++ components/comments/NewThread.tsx | 36 ++++ components/comments/NewThreadCursor.tsx | 12 ++ components/comments/PinnedComposer.tsx | 7 + components/comments/PinnedThread.tsx | 7 +- components/cursor/CursorChat.tsx | 72 +++---- components/cursor/LiveCursors.tsx | 1 + components/settings/Alignment.tsx | 34 ---- components/users/ActiveUsers.tsx | 12 ++ lib/canvas.ts | 84 ++++++++ lib/useMaxZIndex.ts | 3 + lib/utils.ts | 1 - types/type.ts | 13 ++ 21 files changed, 594 insertions(+), 110 deletions(-) delete mode 100644 components/settings/Alignment.tsx diff --git a/app/App.tsx b/app/App.tsx index 574096c..d69c9ea 100644 --- a/app/App.tsx +++ b/app/App.tsx @@ -26,25 +26,106 @@ import { defaultNavElement } from "@/constants"; import { ActiveElement, Attributes } from "@/types/type"; function Home() { + /** + * useUndo and useRedo are hooks provided by Liveblocks that allow you to + * undo and redo mutations. + * + * useUndo: https://liveblocks.io/docs/api-reference/liveblocks-react#useUndo + * useRedo: https://liveblocks.io/docs/api-reference/liveblocks-react#useRedo + */ const undo = useUndo(); const redo = useRedo(); + + /** + * useStorage is a hook provided by Liveblocks that allows you to store + * data in a key-value store and automatically sync it with other users + * i.e., subscribes to updates to that selected data + * + * useStorage: https://liveblocks.io/docs/api-reference/liveblocks-react#useStorage + * + * Over here, we are storing the canvas objects in the key-value store. + */ const canvasObjects = useStorage((root) => root.canvasObjects); + /** + * canvasRef is a reference to the canvas element that we'll use to initialize + * the fabric canvas. + * + * fabricRef is a reference to the fabric canvas that we use to perform + * operations on the canvas. It's a copy of the created canvas so we can use + * it outside the canvas event listeners. + */ const canvasRef = useRef(null); const fabricRef = useRef(null); + /** + * isDrawing is a boolean that tells us if the user is drawing on the canvas. + * We use this to determine if the user is drawing or not + * i.e., if the freeform drawing mode is on or not. + */ const isDrawing = useRef(false); + + /** + * shapeRef is a reference to the shape that the user is currently drawing. + * We use this to update the shape's properties when the user is + * drawing/creating shape + */ const shapeRef = useRef(null); + + /** + * selectedShapeRef is a reference to the shape that the user has selected. + * For example, if the user has selected the rectangle shape, then this will + * be set to "rectangle". + * + * We're using refs here because we want to access these variables inside the + * event listeners. We don't want to lose the values of these variables when + * the component re-renders. Refs help us with that. + */ const selectedShapeRef = useRef(null); + + /** + * activeObjectRef is a reference to the active/selected object in the canvas + * + * We want to keep track of the active object so that we can keep it in + * selected form when user is editing the width, height, color etc + * properties/attributes of the object. + * + * Since we're using live storage to sync shapes across users in real-time, + * we have to re-render the canvas when the shapes are updated. + * Due to this re-render, the selected shape is lost. We want to keep track + * of the selected shape so that we can keep it selected when the + * canvas re-renders. + */ const activeObjectRef = useRef(null); + /** + * imageInputRef is a reference to the input element that we use to upload + * an image to the canvas. + * + * We want image upload to happen when clicked on the image item from the + * dropdown menu. So we're using this ref to trigger the click event on the + * input element when the user clicks on the image item from the dropdown. + */ const imageInputRef = useRef(null); + + /** + * activeElement is an object that contains the name, value and icon of the + * active element in the navbar. + */ const [activeElement, setActiveElement] = useState({ name: "", value: "", icon: "", }); + /** + * elementAttributes is an object that contains the attributes of the selected + * element in the canvas. + * + * We use this to update the attributes of the selected element when the user + * is editing the width, height, color etc properties/attributes of the + * object. + */ const [elementAttributes, setElementAttributes] = useState({ width: "", height: "", @@ -55,72 +136,154 @@ function Home() { stroke: "#aabbcc", }); + /** + * deleteShapeFromStorage is a mutation that deletes a shape from the + * key-value store of liveblocks. + * useMutation is a hook provided by Liveblocks that allows you to perform + * mutations on liveblocks data. + * + * useMutation: https://liveblocks.io/docs/api-reference/liveblocks-react#useMutation + * delete: https://liveblocks.io/docs/api-reference/liveblocks-client#LiveMap.delete + * get: https://liveblocks.io/docs/api-reference/liveblocks-client#LiveMap.get + * + * We're using this mutation to delete a shape from the key-value store when + * the user deletes a shape from the canvas. + */ const deleteShapeFromStorage = useMutation(({ storage }, shapeId) => { + /** + * canvasObjects is a Map that contains all the shapes in the key-value. + * Like a store. We can create multiple stores in liveblocks. + * + * delete: https://liveblocks.io/docs/api-reference/liveblocks-client#LiveMap.delete + */ const canvasObjects = storage.get("canvasObjects"); canvasObjects.delete(shapeId); }, []); + /** + * deleteAllShapes is a mutation that deletes all the shapes from the + * key-value store of liveblocks. + * + * delete: https://liveblocks.io/docs/api-reference/liveblocks-client#LiveMap.delete + * get: https://liveblocks.io/docs/api-reference/liveblocks-client#LiveMap.get + * + * We're using this mutation to delete all the shapes from the key-value store when the user clicks on the reset button. + */ const deleteAllShapes = useMutation(({ storage }) => { + // get the canvasObjects store const canvasObjects = storage.get("canvasObjects"); + // if the store doesn't exist or is empty, return if (!canvasObjects || canvasObjects.size === 0) return true; + // delete all the shapes from the store for (const [key, value] of canvasObjects.entries()) { canvasObjects.delete(key); } + // return true if the store is empty return canvasObjects.size === 0; }, []); + /** + * syncShapeInStorage is a mutation that syncs the shape in the key-value + * store of liveblocks. + * + * We're using this mutation to sync the shape in the key-value store + * whenever user performs any action on the canvas such as drawing, moving + * editing, deleting etc. + */ const syncShapeInStorage = useMutation(({ storage }, object) => { + // if the passed object is null, return if (!object) return; const { objectId } = object; + /** + * Turn Fabric object (kclass) into JSON format so that we can store it in the + * key-value store. + */ const shapeData = object.toJSON(); shapeData.objectId = objectId; const canvasObjects = storage.get("canvasObjects"); + /** + * set is a method provided by Liveblocks that allows you to set a value + * + * set: https://liveblocks.io/docs/api-reference/liveblocks-client#LiveMap.set + */ canvasObjects.set(objectId, shapeData); }, []); + /** + * Set the active element in the navbar and perform the action based + * on the selected element. + * + * @param elem + */ const handleActiveElement = (elem: ActiveElement) => { setActiveElement(elem); switch (elem?.value) { + // delete all the shapes from the canvas case "reset": + // clear the storage deleteAllShapes(); + // clear the canvas fabricRef.current?.clear(); + // set "select" as the active element setActiveElement(defaultNavElement); break; + // delete the selected shape from the canvas case "delete": + // delete it from the canvas handleDelete(fabricRef.current as any, deleteShapeFromStorage); + // set "select" as the active element setActiveElement(defaultNavElement); break; + // upload an image to the canvas case "image": + // trigger the click event on the input element which opens the file dialog imageInputRef.current?.click(); + /** + * set drawing mode to false + * If the user is drawing on the canvas, we want to stop the + * drawing mode when clicked on the image item from the dropdown. + */ isDrawing.current = false; + if (fabricRef.current) { + // disable the drawing mode of canvas fabricRef.current.isDrawingMode = false; } break; + // for comments, do nothing case "comments": break; default: + // set the selected shape to the selected element selectedShapeRef.current = elem?.value as string; break; } }; useEffect(() => { + // initialize the fabric canvas const canvas = initializeFabric({ canvasRef, fabricRef, }); + /** + * listen to the mouse down event on the canvas which is fired when the + * user clicks on the canvas + * + * Event inspector: http://fabricjs.com/events + * Event list: http://fabricjs.com/docs/fabric.Canvas.html#fire + */ canvas.on("mouse:down", (options) => { handleCanvasMouseDown({ options, @@ -131,6 +294,13 @@ function Home() { }); }); + /** + * listen to the mouse move event on the canvas which is fired when the + * user moves the mouse on the canvas + * + * Event inspector: http://fabricjs.com/events + * Event list: http://fabricjs.com/docs/fabric.Canvas.html#fire + */ canvas.on("mouse:move", (options) => { handleCanvaseMouseMove({ options, @@ -142,6 +312,13 @@ function Home() { }); }); + /** + * listen to the mouse up event on the canvas which is fired when the + * user releases the mouse on the canvas + * + * Event inspector: http://fabricjs.com/events + * Event list: http://fabricjs.com/docs/fabric.Canvas.html#fire + */ canvas.on("mouse:up", () => { handleCanvasMouseUp({ canvas, @@ -154,6 +331,14 @@ function Home() { }); }); + /** + * listen to the path created event on the canvas which is fired when + * the user creates a path on the canvas using the freeform drawing + * mode + * + * Event inspector: http://fabricjs.com/events + * Event list: http://fabricjs.com/docs/fabric.Canvas.html#fire + */ canvas.on("path:created", (options) => { handlePathCreated({ options, @@ -161,6 +346,15 @@ function Home() { }); }); + /** + * listen to the object modified event on the canvas which is fired + * when the user modifies an object on the canvas. Basically, when the + * user changes the width, height, color etc properties/attributes of + * the object or moves the object on the canvas. + * + * Event inspector: http://fabricjs.com/events + * Event list: http://fabricjs.com/docs/fabric.Canvas.html#fire + */ canvas.on("object:modified", (options) => { handleCanvasObjectModified({ options, @@ -168,12 +362,26 @@ function Home() { }); }); + /** + * listen to the object moving event on the canvas which is fired + * when the user moves an object on the canvas. + * + * Event inspector: http://fabricjs.com/events + * Event list: http://fabricjs.com/docs/fabric.Canvas.html#fire + */ canvas?.on("object:moving", (options) => { handleCanvasObjectMoving({ options, }); }); + /** + * listen to the selection created event on the canvas which is fired + * when the user selects an object on the canvas. + * + * Event inspector: http://fabricjs.com/events + * Event list: http://fabricjs.com/docs/fabric.Canvas.html#fire + */ canvas.on("selection:created", (options) => { handleCanvasSelectionCreated({ options, @@ -181,6 +389,13 @@ function Home() { }); }); + /** + * listen to the scaling event on the canvas which is fired when the + * user scales an object on the canvas. + * + * Event inspector: http://fabricjs.com/events + * Event list: http://fabricjs.com/docs/fabric.Canvas.html#fire + */ canvas.on("object:scaling", (options) => { handleCanvasObjectScaling({ options, @@ -188,6 +403,13 @@ function Home() { }); }); + /** + * listen to the mouse wheel event on the canvas which is fired when + * the user scrolls the mouse wheel on the canvas. + * + * Event inspector: http://fabricjs.com/events + * Event list: http://fabricjs.com/docs/fabric.Canvas.html#fire + */ canvas.on("mouse:wheel", (options) => { handleCanvasZoom({ options, @@ -195,12 +417,25 @@ function Home() { }); }); + /** + * listen to the resize event on the window which is fired when the + * user resizes the window. + * + * We're using this to resize the canvas when the user resizes the + * window. + */ window.addEventListener("resize", () => { handleResize({ fabricRef, }); }); + /** + * listen to the key down event on the window which is fired when the + * user presses a key on the keyboard. + * + * We're using this to perform some actions like delete, copy, paste, etc when the user presses the respective keys on the keyboard. + */ window.addEventListener("keydown", (e) => handleKeyDown({ e, @@ -212,9 +447,18 @@ function Home() { }) ); + // dispose the canvas and remove the event listeners when the component unmounts return () => { + /** + * dispose is a method provided by Fabric that allows you to dispose + * the canvas. It clears the canvas and removes all the event + * listeners + * + * dispose: http://fabricjs.com/docs/fabric.Canvas.html#dispose + */ canvas.dispose(); + // remove the event listeners window.removeEventListener("resize", () => { handleResize({ fabricRef, @@ -232,8 +476,9 @@ function Home() { }) ); }; - }, [canvasRef]); + }, [canvasRef]); // run this effect only once when the component mounts and the canvasRef changes + // render the canvas when the canvasObjects from live storage changes useEffect(() => { renderCanvas({ fabricRef, @@ -248,7 +493,9 @@ function Home() { imageInputRef={imageInputRef} activeElement={activeElement} handleImageUpload={(e: any) => { + // prevent the default behavior of the input element e.stopPropagation(); + handleImageUpload({ file: e.target.files[0], canvas: fabricRef as any, diff --git a/app/Room.tsx b/app/Room.tsx index e0d10a0..8396259 100644 --- a/app/Room.tsx +++ b/app/Room.tsx @@ -3,14 +3,31 @@ import { LiveMap } from "@liveblocks/client"; import { RoomProvider } from "@/liveblocks.config"; import { ClientSideSuspense } from "@liveblocks/react"; + import Loader from "@/components/Loader"; export function Room({ children }: { children: React.ReactNode }) { return ( diff --git a/app/page.tsx b/app/page.tsx index dce90be..99f0019 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,5 +1,10 @@ import dynamic from "next/dynamic"; +/** + * disable ssr to avoid pre-rendering issues of Next.js + * + * we're doing this because we're using a canvas element that can't be pre-rendered by Next.js on the server + */ const App = dynamic(() => import("./App"), { ssr: false }); export default App; diff --git a/components/LeftSidebar.tsx b/components/LeftSidebar.tsx index 49530fb..0784e6a 100644 --- a/components/LeftSidebar.tsx +++ b/components/LeftSidebar.tsx @@ -6,6 +6,7 @@ import Image from "next/image"; import { getShapeInfo } from "@/lib/utils"; function LeftSidebar({ allShapes }: { allShapes: Array }) { + // memoize the result of this function so that it doesn't change on every render but only when there are new shapes const memoizedShapes = useMemo(() => { return (
diff --git a/components/Live.tsx b/components/Live.tsx index 53447af..8643163 100644 --- a/components/Live.tsx +++ b/components/Live.tsx @@ -35,15 +35,37 @@ function Live({ undo: () => void; redo: () => void; }) { + /** + * useOthers returns the list of other users in the room. + * + * useOthers: https://liveblocks.io/docs/api-reference/liveblocks-react#useOthers + */ const others = useOthers(); + + /** + * useMyPresence returns the presence of the current user in the room. + * It also returns a function to update the presence of the current user. + * + * useMyPresence: https://liveblocks.io/docs/api-reference/liveblocks-react#useMyPresence + */ const [{ cursor }, updateMyPresence] = useMyPresence() as any; + + /** + * useBroadcastEvent is used to broadcast an event to all the other users in the room. + * + * useBroadcastEvent: https://liveblocks.io/docs/api-reference/liveblocks-react#useBroadcastEvent + */ const broadcast = useBroadcastEvent(); + // store the reactions created on mouse click const [reactions, setReactions] = useState([]); + + // track the state of the cursor (hidden, chat, reaction, reaction selector) const [cursorState, setCursorState] = useState({ mode: CursorMode.Hidden, }); + // set the reaction of the cursor const setReaction = useCallback((reaction: string) => { setCursorState({ mode: CursorMode.Reaction, reaction, isPressed: false }); }, []); @@ -55,12 +77,14 @@ function Live({ ); }, 1000); + // Broadcast the reaction to other users (every 100ms) useInterval(() => { if ( cursorState.mode === CursorMode.Reaction && cursorState.isPressed && cursor ) { + // concat all the reactions created on mouse click setReactions((reactions) => reactions.concat([ { @@ -70,6 +94,8 @@ function Live({ }, ]) ); + + // Broadcast the reaction to other users broadcast({ x: cursor.x, y: cursor.y, @@ -78,6 +104,12 @@ function Live({ } }, 100); + /** + * useEventListener is used to listen to events broadcasted by other + * users. + * + * useEventListener: https://liveblocks.io/docs/api-reference/liveblocks-react#useEventListener + */ useEventListener((eventData) => { const event = eventData.event as ReactionEvent; setReactions((reactions) => @@ -91,6 +123,7 @@ function Live({ ); }); + // Listen to keyboard events to change the cursor state useEffect(() => { function onKeyUp(e: KeyboardEvent) { if (e.key === "/") { @@ -122,13 +155,17 @@ function Live({ }; }, [updateMyPresence]); + // Listen to mouse events to change the cursor state const handlePointerMove = useCallback((event: React.PointerEvent) => { event.preventDefault(); + + // if cursor is not in reaction selector mode, update the cursor position if (cursor == null || cursorState.mode !== CursorMode.ReactionSelector) { // get the cursor position in the canvas const x = event.clientX - event.currentTarget.getBoundingClientRect().x; const y = event.clientY - event.currentTarget.getBoundingClientRect().y; + // broadcast the cursor position to other users updateMyPresence({ cursor: { x, @@ -138,6 +175,7 @@ function Live({ } }, []); + // Hide the cursor when the mouse leaves the canvas const handlePointerLeave = useCallback(() => { setCursorState({ mode: CursorMode.Hidden, @@ -148,6 +186,7 @@ function Live({ }); }, []); + // Show the cursor when the mouse enters the canvas const handlePointerDown = useCallback( (event: React.PointerEvent) => { // get the cursor position in the canvas @@ -160,6 +199,8 @@ function Live({ y, }, }); + + // if cursor is in reaction mode, set isPressed to true setCursorState((state: CursorState) => cursorState.mode === CursorMode.Reaction ? { ...state, isPressed: true } @@ -169,6 +210,7 @@ function Live({ [cursorState.mode, setCursorState] ); + // hide the cursor when the mouse is up const handlePointerUp = useCallback(() => { setCursorState((state: CursorState) => cursorState.mode === CursorMode.Reaction @@ -177,9 +219,8 @@ function Live({ ); }, [cursorState.mode, setCursorState]); + // trigger respective actions when the user clicks on the right menu const handleContextMenuClick = useCallback((key: string) => { - console.log(key); - switch (key) { case "Chat": setCursorState({ @@ -221,6 +262,7 @@ function Live({ > + {/* Render the reactions */} {reactions.map((reaction) => ( ))} + {/* If cursor is in chat mode, show the chat cursor */} {cursor && ( )} + {/* If cursor is in reaction selector mode, show the reaction selector */} {cursorState.mode === CursorMode.ReactionSelector && ( { @@ -248,10 +292,14 @@ function Live({ /> )} + {/* Show the live cursors of other users */} + + {/* Show the comments */} + {/* Show the content of right menu click */} {shortcuts.map((item) => ( + {/* If value is an array means it's a nav element with sub options i.e., dropdown */} {Array.isArray(item.value) ? ( ) : item?.value === "comments" ? ( + // If value is comments, trigger the NewThread component - ))} - - ); -} - -export default Alignment; diff --git a/components/users/ActiveUsers.tsx b/components/users/ActiveUsers.tsx index 732c2b5..410fc76 100644 --- a/components/users/ActiveUsers.tsx +++ b/components/users/ActiveUsers.tsx @@ -8,9 +8,21 @@ import { generateRandomName } from "@/lib/utils"; import { useOthers, useSelf } from "@/liveblocks.config"; const ActiveUsers = () => { + /** + * useOthers returns the list of other users in the room. + * + * useOthers: https://liveblocks.io/docs/api-reference/liveblocks-react#useOthers + */ const others = useOthers(); + + /** + * useSelf returns the current user details in the room + * + * useSelf: https://liveblocks.io/docs/api-reference/liveblocks-react#useSelf + */ const currentUser = useSelf(); + // memoize the result of this function so that it doesn't change on every render but only when there are new users joining the room const memoizedUsers = useMemo(() => { const hasMoreUsers = others.length > 2; diff --git a/lib/canvas.ts b/lib/canvas.ts index cc78537..00ccc01 100644 --- a/lib/canvas.ts +++ b/lib/canvas.ts @@ -15,6 +15,7 @@ import { import { createSpecificShape } from "./shapes"; import { defaultNavElement } from "@/constants"; +// initialize fabric canvas export const initializeFabric = ({ fabricRef, canvasRef, @@ -22,18 +23,22 @@ export const initializeFabric = ({ fabricRef: React.MutableRefObject; canvasRef: React.MutableRefObject; }) => { + // get canvas element const canvasElement = document.getElementById("canvas"); + // create fabric canvas const canvas = new fabric.Canvas(canvasRef.current, { width: canvasElement?.clientWidth, height: canvasElement?.clientHeight, }); + // set canvas reference to fabricRef so we can use it later anywhere outside canvas listener fabricRef.current = canvas; return canvas; }; +// instantiate creation of custom fabric object/shape and add it to canvas export const handleCanvasMouseDown = ({ options, canvas, @@ -41,11 +46,21 @@ export const handleCanvasMouseDown = ({ isDrawing, shapeRef, }: CanvasMouseDown) => { + // get pointer coordinates const pointer = canvas.getPointer(options.e); + + /** + * get target object i.e., the object that is clicked + * findtarget() returns the object that is clicked + * + * findTarget: http://fabricjs.com/docs/fabric.Canvas.html#findTarget + */ const target = canvas.findTarget(options.e, false); + // set canvas drawing mode to false canvas.isDrawingMode = false; + // if selected shape is freeform, set drawing mode to true and return if (selectedShapeRef.current === "freeform") { isDrawing.current = true; canvas.isDrawingMode = true; @@ -55,28 +70,40 @@ export const handleCanvasMouseDown = ({ canvas.isDrawingMode = false; + // if target is the selected shape or active selection, set isDrawing to false if ( target && (target.type === selectedShapeRef.current || target.type === "activeSelection") ) { isDrawing.current = false; + + // set active object to target canvas.setActiveObject(target); + + /** + * setCoords() is used to update the controls of the object + * setCoords: http://fabricjs.com/docs/fabric.Object.html#setCoords + */ target.setCoords(); } else { isDrawing.current = true; + // create custom fabric object/shape and set it to shapeRef shapeRef.current = createSpecificShape( selectedShapeRef.current, pointer as any ); + // if shapeRef is not null, add it to canvas if (shapeRef.current) { + // add: http://fabricjs.com/docs/fabric.Canvas.html#add canvas.add(shapeRef.current); } } }; +// handle mouse move event on canvas to draw shapes with different dimensions export const handleCanvaseMouseMove = ({ options, canvas, @@ -85,12 +112,17 @@ export const handleCanvaseMouseMove = ({ shapeRef, syncShapeInStorage, }: CanvasMouseMove) => { + // if selected shape is freeform, return if (!isDrawing.current) return; if (selectedShapeRef.current === "freeform") return; canvas.isDrawingMode = false; + + // get pointer coordinates const pointer = canvas.getPointer(options.e); + // depending on the selected shape, set the dimensions of the shape stored in shapeRef in previous step of handelCanvasMouseDown + // calculate shape dimensions based on pointer coordinates switch (selectedShapeRef?.current) { case "rectangle": shapeRef.current?.set({ @@ -129,13 +161,17 @@ export const handleCanvaseMouseMove = ({ break; } + // render objects on canvas + // renderAll: http://fabricjs.com/docs/fabric.Canvas.html#renderAll canvas.renderAll(); + // sync shape in storage if (shapeRef.current?.objectId) { syncShapeInStorage(shapeRef.current); } }; +// handle mouse up event on canvas to stop drawing shapes export const handleCanvasMouseUp = ({ canvas, isDrawing, @@ -148,12 +184,15 @@ export const handleCanvasMouseUp = ({ isDrawing.current = false; if (selectedShapeRef.current === "freeform") return; + // sync shape in storage as drawing is stopped syncShapeInStorage(shapeRef.current); + // set everything to null shapeRef.current = null; activeObjectRef.current = null; selectedShapeRef.current = null; + // if canvas is not in drawing mode, set active element to default nav element after 700ms if (!canvas.isDrawingMode) { setTimeout(() => { setActiveElement(defaultNavElement); @@ -161,6 +200,7 @@ export const handleCanvasMouseUp = ({ } }; +// update shape in storage when object is modified export const handleCanvasObjectModified = ({ options, syncShapeInStorage, @@ -175,30 +215,40 @@ export const handleCanvasObjectModified = ({ } }; +// update shape in storage when path is created when in freeform mode export const handlePathCreated = ({ options, syncShapeInStorage, }: CanvasPathCreated) => { + // get path object const path = options.path; if (!path) return; + // set unique id to path object path.set({ objectId: uuid4(), }); + // sync shape in storage syncShapeInStorage(path); }; +// check how object is moving on canvas and restrict it to canvas boundaries export const handleCanvasObjectMoving = ({ options, }: { options: fabric.IEvent; }) => { + // get target object which is moving const target = options.target as fabric.Object; + + // target.canvas is the canvas on which the object is moving const canvas = target.canvas as fabric.Canvas; + // set coordinates of target object target.setCoords(); + // restrict object to canvas boundaries (horizontal) if (target && target.left) { target.left = Math.max( 0, @@ -209,6 +259,7 @@ export const handleCanvasObjectMoving = ({ ); } + // restrict object to canvas boundaries (vertical) if (target && target.top) { target.top = Math.max( 0, @@ -220,16 +271,21 @@ export const handleCanvasObjectMoving = ({ } }; +// set element attributes when element is selected export const handleCanvasSelectionCreated = ({ options, setElementAttributes, }: CanvasSelectionCreated) => { + // if no element is selected, return if (!options?.selected) return; + // get the selected element const selectedElement = options?.selected[0]; + // if only one element is selected, set element attributes if (selectedElement && options.selected.length === 1) { setElementAttributes({ + // getScaledWidth() and getScaledHeight() are used to get the scaled dimensions of the object. These are the latest dimensions of the object after scaling width: Math.round(selectedElement?.getScaledWidth() || 0).toString(), height: Math.round(selectedElement?.getScaledHeight() || 0).toString(), fill: selectedElement?.fill?.toString() || "", @@ -244,6 +300,7 @@ export const handleCanvasSelectionCreated = ({ } }; +// update element attributes when element is scaled export const handleCanvasObjectScaling = ({ options, setElementAttributes, @@ -257,25 +314,46 @@ export const handleCanvasObjectScaling = ({ })); }; +// render canvas objects coming from storage on canvas export const renderCanvas = ({ fabricRef, canvasObjects, activeObjectRef, }: RenderCanvas) => { + // clear canvas fabricRef.current?.clear(); + // render all objects on canvas Array.from(canvasObjects, ([objectId, objectData]) => { + /** + * enlivenObjects() is used to render objects on canvas. + * It takes two arguments: + * 1. objectData: object data to render on canvas + * 2. callback: callback function to execute after rendering objects + * on canvas + * + * enlivenObjects: http://fabricjs.com/docs/fabric.util.html#.enlivenObjectEnlivables + */ fabric.util.enlivenObjects( [objectData], (enlivenedObjects: fabric.Object[]) => { enlivenedObjects.forEach((enlivenedObj) => { + // if element is active, keep it in active state so that it can be edited further if (activeObjectRef.current?.objectId === objectId) { fabricRef.current?.setActiveObject(enlivenedObj); } + // add object to canvas fabricRef.current?.add(enlivenedObj); }); }, + /** + * specify namespace of the object for fabric to render it on canvas + * A namespace is a string that is used to identify the type of + * object. + * + * Fabric Namespace: http://fabricjs.com/docs/fabric.html + */ "fabric" ); }); @@ -283,6 +361,7 @@ export const renderCanvas = ({ fabricRef.current?.renderAll(); }; +// resize canvas dimensions on window resize export const handleResize = ({ fabricRef, }: { @@ -301,6 +380,7 @@ export const handleResize = ({ canvas?.renderAll(); }; +// zoom canvas on mouse scroll export const handleCanvasZoom = ({ options, canvas, @@ -316,9 +396,13 @@ export const handleCanvasZoom = ({ const maxZoom = 1; const zoomStep = 0.001; + // calculate zoom based on mouse scroll wheel with min and max zoom zoom = Math.min(Math.max(minZoom, zoom + delta * zoomStep), maxZoom); + // set zoom to canvas + // zoomToPoint: http://fabricjs.com/docs/fabric.Canvas.html#zoomToPoint canvas.zoomToPoint({ x: options.e.offsetX, y: options.e.offsetY }, zoom); + options.e.preventDefault(); options.e.stopPropagation(); }; diff --git a/lib/useMaxZIndex.ts b/lib/useMaxZIndex.ts index 1a70ec8..d34db5f 100644 --- a/lib/useMaxZIndex.ts +++ b/lib/useMaxZIndex.ts @@ -1,9 +1,12 @@ import { useMemo } from "react"; import { useThreads } from "@/liveblocks.config"; +// Returns the highest z-index of all threads export function useMaxZIndex() { + // get all threads const { threads } = useThreads(); + // calculate the max z-index return useMemo(() => { let max = 0; for (const thread of threads) { diff --git a/lib/utils.ts b/lib/utils.ts index ee07fdb..2a6f906 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -98,7 +98,6 @@ export const getShapeInfo = (shapeType: string) => { } }; -// write an exportToPdf function that takes in the canvas and downloads the canvas as a pdf while preserving the canvas elements size and position as it is on the canvas export const exportToPdf = () => { const canvas = document.querySelector("canvas"); diff --git a/types/type.ts b/types/type.ts index 9c2fa83..bc1c8eb 100644 --- a/types/type.ts +++ b/types/type.ts @@ -175,3 +175,16 @@ export type RenderCanvas = { canvasObjects: any; activeObjectRef: any; }; + +export type CursorChatProps = { + cursor: { x: number; y: number }; + cursorState: CursorState; + setCursorState: (cursorState: CursorState) => void; + updateMyPresence: ( + presence: Partial<{ + cursor: { x: number; y: number }; + cursorColor: string; + message: string; + }> + ) => void; +}; From 4e3d4b1fcdc65320a93035a4492352550ea00e74 Mon Sep 17 00:00:00 2001 From: sujatagunale Date: Mon, 29 Jan 2024 16:16:42 +0530 Subject: [PATCH 2/6] change icon --- components/cursor/Cursor.tsx | 1 - public/assets/delete.svg | 20 +------------------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/components/cursor/Cursor.tsx b/components/cursor/Cursor.tsx index eb09f41..b9a44bc 100644 --- a/components/cursor/Cursor.tsx +++ b/components/cursor/Cursor.tsx @@ -1,5 +1,4 @@ import CursorSVG from "@/public/assets/CursorSVG"; -import React from "react"; type Props = { color: string; diff --git a/public/assets/delete.svg b/public/assets/delete.svg index 81d031a..283e2b0 100644 --- a/public/assets/delete.svg +++ b/public/assets/delete.svg @@ -1,19 +1 @@ - - - - - delete [#1487] - Created with Sketch. - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file From d4859e9d7e408a00dc1008211f036de71cab996d Mon Sep 17 00:00:00 2001 From: sujatagunale Date: Mon, 29 Jan 2024 16:26:41 +0530 Subject: [PATCH 3/6] navbar fix --- components/Navbar.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/components/Navbar.tsx b/components/Navbar.tsx index ea79f03..54ea57f 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -33,6 +33,10 @@ function Navbar({ {navElements.map((item: ActiveElement | any) => (
  • { + if (Array.isArray(item.value)) return; + handleActiveElement(item); + }} className={`group px-2.5 py-5 flex justify-center items-center ${ isActive(item.value) @@ -53,10 +57,7 @@ function Navbar({ ) : item?.value === "comments" ? ( // If value is comments, trigger the NewThread component -