All right, so now I think it's time for us to finally create the functionality that when we click on a rectangle and then click on our canvas we actually render the rectangle. So, first things first. Let's go ahead back inside of our canvas and one thing that I forgot here is to actually apply the coordinates of the camera to our SVG element right here. So right now our on wheel is changing the movement but nothing is actually happening. So let's go ahead and find our G element here and give it a style transform, open backticks, translate, open parenthesis.
For the first argument we're going to use camera X coordinate and write pixels and then we're going to write comma camera Y coordinate pixels like this. Right now if you try scrolling nothing is going to happen but later when we actually render something on the canvas we're gonna see whether this is working or not Great! Now let's go to the top of our app here and let's define a constant max layers and let's put it at a 100 So for this tutorial I'm gonna limit the layers to a hundred. This is a good practice to have. You can increase it to a thousand to a million if you want to but it's a good practice to have a limit of layers inside of your canvas here.
Now let's go ahead and let's revisit our types for a second. So types.convas.ts and let's go all the way to the bottom here and I want to export a type called layer. So a layer is going to be all of our possible layers which we've defined above. So that's going to be our Rectangle layer, our Ellipse layer, our Path layer, our Text layer and our note layer. So just confirm that you have the rectangle layer, confirm that you have the ellipse layer, the path layer and the last one is the text layer and of course the note layer.
Great! Now let's go ahead inside of liveblocks.config.ts and in here, let's go ahead and from LiveBlocksClient, let's add some additional imports here. So we are going to need LiveList, LiveMap and LiveObject. And from our types, we are going to need, so from types canvas, we are going to need our newly created object. So we are going to need LiveList, LiveMap and LiveObject.
And from our types, we are going to need... So from types canvas, we are going to need our newly created layer and color. And now let's go ahead and find the storage. So let me just find where that is. There we go.
Type storage just above the user meta and just below the presence where we added our cursor. And I'm gonna remove the comment from here. And inside, I'm gonna go ahead and write layers to be a type of line map open pointy brackets, string and live object open pointy brackets again, layer. Like this. And then layer IDs are going to be live list and string inside like that.
Great! Now let's go ahead inside of our reusable room component so components room and in here let's go ahead and add initial storage to have layers to be new live map from LiveBlocks client so just make sure you add this import. And let's actually import everything we need from LiveBlocksClient. So LiveMap and LiveObject. LiveMap of...
I did this twice. Sorry, LiveList is one of those. And let's also import layer from types canvas so live map, live list and live object from live blocks client and layer from types canvas and now in here we can go ahead and define the initial storage layers to be new live map passing the string and then live object and passing the layer and execute it like this and layer IDs are going to be new live list like that. Great! Now let's head back inside of our canvas component.
So that's inside of the App, Board, Board ID, Canvas right here. And let's go ahead and let's define our layer IDs here. So const layer IDs are going to be used storage, which we can import from add slash live blocks dot config. So let me show you that right here. So the same place where we have history, undo, redo, mutation, and other stuff.
And from here, from the root, we're simply going to extract root layer IDs. So layer IDs are going to be information about everything that we have to display on our canvas. So we are going to do a map iteration over those layer IDs and depending on the layer type we're going to know whether to show the rectangle, sticky note, text, pencil or an ellipse and depending on even more information like width, height and the X and Y coordinates we're going to know exactly where and how to position them on our canvas. Great! And besides the layer IDs, let's go ahead and below the camera let's add a new state for our last used color.
And set last used color. That's gonna be useState with a type of color from types canvas. So make sure you import color from types canvas, which is just a very simple RGB object. So R is going to be zero, G is going to zero, and B is going to be zero. This represents the black color as the initial color we are going to paint everything with.
Great. Now let's create a function to insert a layer. So I'm gonna do that after this... Well, actually let's do it right here after we initialize this hooks here. I'm gonna write const insertLayer to be useMutation, which we already have.
And in here, I'm gonna go ahead and extract storage and set my presence and from the second argument I'm gonna extract layer type which is gonna be a type of layer type which you can import from add slash types canvas so again let's just collapse all of this nicely so we can refresh exactly what we need from types canvas so make sure you add layer type as the second argument and now we're going to define what layer types can be added here. So it's going to be layer type dot ellipse, it's going to be layer type dot rectangle, it's going to be layer type dot text and it's going to be layer type dot note but it's not going to be layer type dot path. That's going to work on its own insert function because remember drawing is gonna be a little bit different. So that's the second argument. So make sure you write all of this in one line.
And the third argument is going to be a point, sorry, position, which is a type of point. And I think we already have point imported. Let me go here. Do we have a type point? Let me just find it here.
Point, we do have a type point. Do I export it? I do, but I don't import it. So make sure you import type point from here so you can work with it and it feels like I didn't assign this properly For some reason this doesn't seem to be working. Point refers to a type but is being used as a value.
Okay, how about we just continue doing this. Extract this into a function and add the dependency array. Okay, it looks like I was just missing this part. So point is now definitely referring to this import which I've added here. So just ensure that you have the import for point and if I don't import that great I have an error here perfect, so make sure you import point from types canvas as I did right here point great, and now what we are going to do with this information is first let's get all the layers so const live layers are going to be our storage.getLayers like this and then we're going to check if we've hit a limit on our layers so if LiveLayers.size is higher or equal than maxLayers which we've defined at the top in a constant which is just a number 100 we are going to break this function.
Otherwise let's go ahead and let's load the layer IDs. So const live layer IDs are going to be storage.getLayerIDs and then let's go ahead and define const layer ID to be an ID and for this I'm going to use nano ID so let's go inside of the terminal quickly and let's just do npm install nano ID like this great and now we can import nano ID and let me just go ahead to the top here and add import from nano ID. I'm not sure it is a default import nano ID. I think that this Yes, this looks fine. So layer ID is nano ID and then let's actually create the new layer.
So const layer is going to be new live object from live blocks client. So just make sure you add this import and I'm going to move that with the types here. Not with the types, with the global imports here. So import live object from live blocks client. So now that we have a new layer ID, let's go ahead and give this a type of layer type, which is gonna come from the second argument right here in this insert layer function then we're going to have x to be our position.x, y position.y and in here we're going to define the default height and width of this element.
So height is going to be 100, width is going to be 100 as well. You can of course change this later. You can even change it depending on the layer type. So you have all the information you want here. So if you, for example, if layer type is layer type.text, For example, you can use the width to be 500 initially.
Right? You can just play around with that if you want to, but for now just follow along exactly like I do. So you are not death prone to errors, right? And fill is going to be last used color. So that's how we're going to define the default color.
And once we have our new layer, we can then push this to our live layer IDs. So live layer IDs dot push is going to get a new layer ID and live layers.setLayerID and layer object. Like this. And finally let's call setMyPresence selection an array and layer ID inside and addToHistory is going to be true. So let's ignore this error for now.
We need to add the selection We need to add selection to our live block config and let's call set state here Yes set canvas state mode canvas mode dot none like this and let's add last used color in the dependency array here perfect so now let's go ahead and resolve this little selection issue Let's go back inside of our Live blocks config.ts and in the presence here. Let's go ahead below the cursor and add selection To be string and an array So an array of strings and that resolves this issue with selection. But now we have to go back inside of our reusable room component and in here very simply we're gonna add the selection to be an empty array like this. And then we can go back inside of canvas right here. Great, so now that we have our method to insert a new layer we have to create a method called OnMouseUp I believe because that's the handler.
Actually, let's call it OnPointerUp. And then depending on the current layer mode and canvas state mode we're gonna call the insert layer if we are one of these layer types otherwise we're gonna call some other methods so let me go ahead and actually do that instead of just talking about it So make sure that you have defined the insert layer method here. And then we have onPointer.move, we have onPointer.leave and now let's add const onPointer.up that's also going to be useMutation and it's actually not going to extract anything from the first argument it's just going to work with the event here and let's go ahead and add an empty array inside for now, later it's going to be filled and then what we are going to do is write const point to be pointer event to canvas so we're gonna reuse that method and pass in our event and the camera so we know exactly where to insert something and then we're gonna write if canvasState.mode is equal to canvasMode.inserting we're gonna call insertLayer and pass in canvasState.layerType and the point, like that. Else, we're gonna call setCanvasState and we're gonna pass in the mode to be canvasMode.none.
And let's see if there is something we can already pass inside of this dependency array. So how about we pass in the camera and the canvas state because I think we rely on them. We rely on canvas and we rely on canvas state. Great. And let's also do one more thing.
So after this if clause finishes let's add history.resume here and let's also add history itself in here and also insert layer because that also uses useMutation. Perfect. And now let's revisit our insert layer here just to see if there is something we could add inside of this dependency array. So insert layer or did we already add something? Insert layer has the last used color and yeah, that's actually the only thing we rely on here because everything else we fetch inside of this method.
Great and now what we have to do is we have to add that on pointer up method to our SVG. So let's go ahead here to our SVG and add on pointer up on pointer up like this so the best way to try this out is by console logging so I'm gonna go ahead and console log our points and our mode which is gonna be canvas state dot mode and let's also add layer type canvas state layer do we have canvas state? Oh we only have mode. Ok let's leave it like this So let's try it out. I'm going to go ahead and open my terminal here.
I will select a rectangle. And there we go. When I click, I have a mode four and I have a point right here. Perfect. So mode four is an enum, which I believe represents that something is being inserted and I don't know if you noticed but once I clicked on something, so once I pressed something Let me just wait for this to load.
Right now my undo and redo are disabled but once I press you can see that I now have undo because I finally have my history and I can undo and redo. Great! So something obviously exists inside of here but it's not currently visible. So we did a good job with our insert layer and with our on pointer up. But now what we have to do to check whether this point is actually correct where we inserted that, whether our camera translation is working, we have to add an iteration over our layer IDs.
So let's go just above the cursor presence inside of the G element and go over our layer IDs dot map. Let's get the individual layer ID and we're gonna render layer preview component. Let's pass in the key to be layer ID. Let's pass in the ID to be layer ID. And on layer pointer down to be, well for now just an empty arrow function and selection color for now let's also let's make this empty for now as well and now let's go inside of this underscore components and create a new file layer preview.vsx let's import use client sorry let's mark this as use client and let's export const layer preview here and let's return a div.
So it really doesn't matter we just want to fix the import error and then we can import that from .slash layer preview like this. Let's go back here. Let's go back inside of layer preview now and let's add these props here. So interface layer preview props is gonna have an ID of string. It's gonna have onLayer.down an event of React.PointerEvent and layer ID, which is a string, and it's going to call a void.
And selectionColor is simply going to be a string. Like that. And then you can extract all of those layer preview props. ID on layer pointer down and selection color. So let's explain this props a little bit.
So selection color, we're gonna generate that in a moment. Basically what selection color is gonna be is it's gonna be an indicator that when another user has clicked on a rectangle for example, we're gonna make sure that all other users can see an indicator that someone else is moving or editing that object and that color is gonna be matching their color ID. So just for now I'm gonna make this a hex code of black, right? So just three zeros like this. And on pointer down, well for now we don't really have to resolve that immediately.
We can also just make this completely empty and add to do fixed types. Let's see if that onLayerPointerDown still doesn't do it. Oh, my apologies. Maybe I didn't have to move the types. So onLayerPointerDown.
There we go. That seems to resolve the issue. Great. And now we can just leave this like this. Let's go inside of a layer preview props and let's import use memo.
Sorry let's import memo from react so we can memoize this as well. So we're gonna wrap this entire thing in a memo like this and then we need to add layer preview display name to be layer preview and then let's go ahead and let's find out what layer we are currently iterating over. So for that we're going to use useStorage. So const layer is useStorage from our live blocks config. So we're gonna call useStorage, specifically we're gonna get the root and then root layers get, not layer IDs, layers.get and then the ID of that layer.
And if we cannot find that layer in our storage, we simply aren't going to render this layer preview. There's nothing we can do here. And here's the cool part now. The way we're gonna reuse this Layer Preview component for all of our types, for text, for sticky notes, for rectangles, for all of those things, we are simply gonna do a switch statement over the layer type. So let's write switch layer.type and for now let's just write a case for layer type.rectangle and we need to import layer type from types canvas.
So just make sure you add layer type here so if layer type is rectangle we're gonna go ahead and simply return for now let's just say a div rectangle and then let's add a default we're gonna add a console.warn unknown layer type so in case that happens you are going to know and return null. Like that and that's how we are dynamically going to decide what we are rendering. So now we have to actually create this rectangle object. So let's go inside of underscore components and create rectangle.psx. If you want you can give this components some kind of prefix like rectangle layer or layer rectangle.
And in here, let's create an interface rectangle props to accept an ID, which is a string, a layer, which is gonna be a rectangle layer from types canvas, on pointer down, which is going to have an event of react pointer event and id which is a string and then a void and selection color which is going to be an optional string and then let's go ahead and do export const rectangle and let's assign this props right here so rectangle props we can then extract them and I think I have a typo here so rectangle props id layer on pointer down and selection color And very simply what we're gonna do is we're gonna extract from layer the following x coordinate, y coordinate, width, height and fill. And we know we're gonna have those because of the rectangle layer type schema So we know that if type layer is rectangle, we have the X Y height width and fill I also added the value because I know is going to mess with us later But we're not gonna use value for the rectangle and then we're going to return a rect which is a self-closing tag and in here we're going to give this a class name of drop shadow md We're going to give it an on pointer down to be event on pointer down and passing the event and id style is going to be transform, open backticks, translate, passing X in pixels, comma, Y coordinate in pixels.
We are manually going to set X to be zero here and Y to be zero here. Width is going to be width. Height is going to be height. These are by default 100. If you remember from our insert layer method.
Stroke width is going to be 1 Fill is going to be let's go ahead and manually just add black for now and let's go ahead and give this a stroke of transparent for now. Great. So let's go ahead and see if it's possible to maybe see this already or if we are missing something. So if I add a rectangle and click it looks like nothing is appearing. So let's go ahead and debug a little bit.
First thing that I'm interested in is whether this rectangle ever loads so let's add let's log ID and layer. Will this ever log here? Let's go ahead and click here. Looks like this is never loading so now what I want to do is check inside of my layer preview here so I have a layer so let's console.log layer and let me just add a little comma layer preview so I know where this is coming from. Okay, so it looks like I do have an object here.
I do have a layer right here. And then it looks like it doesn't have the, oh, well, because I never rendered the rectangle. My apologies for that. So let's actually render the rectangle inside of here and we also have to pass in all of the props I completely forgot my apologies so let's render a rectangle from .slash rectangle which we've just created let's pass in the id let's pass in the layer let's pass in on pointer down to be on pointer do we have on pointer down uh-huh on layer pointer down. And selection color.
Selection color. And there we go. Once I did that, so just make sure you imported the rectangle from .slash rectangle. And let me remove my console logs, which I luckily don't need anymore. Let's try this out now.
There we go. You can see that when I click here and I select here, this is where my rectangle appears. And if I click undo, it disappears. And let's try with my camera. As you can see, I can move my camera up and down.
I can completely hide this if I want to. So everything seems to be working just fine. What I'm interested in is whether another user can add their own rectangles. So this what you're seeing is another user, my other browser. And there we go.
In real time you can see them adding their very own rectangles. Perfect! So this is now working. So what we have to do now is we have to create the functionality to move these elements, to select these elements, to transform and resize these elements and also to change their colors. So I'm gonna leave it this chapter like this for now and let's just quickly recap exactly what's going on because it can be a little bit confusing.
So inside of our app folder board, board ID components, we have the canvas. Let's go step by step and see what happens. So we have an on pointer up event which is added to the SVG element which takes up a hundred percent of our screen. So when we click somewhere in the screen what happens is that on pointer up event is triggered. In here we check if we have a canvas state of inserting.
If we do we insert the layer. So that's this action. When I select a rectangle I have changed my canvas state because if you remember in my toolbar when I select my rectangle let's find it there we go I do I call set canvas state and I change the mode to inserting sorry this is sticky note right here rectangle I change the mode to inserting and the layer type to rectangle. And then when the pointer up event fires again, it goes through this if check because canvas state mode is inserting. So I pass in the layer type and the point where the user moved their mouse up, so where they stopped pressing and then the insert layer has that information.
It has the layer type and it has the position. It also has the storage and my presence because of the use mutation which we are using from live blocks. So let me find insert layer. Okay. And then what we do is we confirm that we haven't reached the maximum level of our layers which is just a constant 100 so make sure you have this defined too.
And then we go ahead and generate a new layer using nano ID just to get a random ID and then we use all the information about our current pointer like position and X and Y coordinates. We add some default height and width. We use the last used color which is just an empty state of RGB 0 representing the color black. And then we push that to layer IDs and to live layers which are all controlled by live blocks. And then we update the presence, we add it to history.
So then automatically those hooks, use can undo, use can redo and use history are automatically functional now, which you just saw if I click undo here this one goes away so that works if I click redo it goes back and we reset the canvas state back to the select so after I click rectangle it goes back to select and now we're gonna go ahead and just wrap up some other functionalities like clicking on this, resizing this and collecting a selection net and then it's quite easy from there because we're gonna have a bunch of features already and we're just gonna have to modify different things inside of this layer preview component. So if switch case goes to layer ellipse, we're gonna render ellipse. Sticky note, we render the sticky note. Path, we render the path. So great, great job.
You are really doing great. This is a really hard project. I've had a really hard time just grasping the concept of the live board. So if you've got up to this point with or without errors, great, great job.