So in order to implement the pencil mode, let's start from the beginning. So the beginning starts with the event onPointerDown. So let's go ahead and see what existing code we have around that. So inside of Board.Components.Canvas, let's go ahead and find our event on our SVG element on pointer down. So in here it looks like we already prepared a little to do here.
So let's go ahead and write if canvasState.mode is identical to canvasMode.pencil in that case let's break the function but before we do that let's console.log and let's write drawing just so we see whether this is working or not. So I'm gonna go inside of here. If I just randomly click nothing happens but if I select a pencil and I click it says that I'm drawing. Exactly what we want. So instead of console.log drawing we're gonna go ahead and call a function startDrawing and we're gonna pass in the point and we're gonna pass in the event.pressure like that.
So now let's go ahead and let's add this start drawing to our dependency array so we don't forget to do that so right now start drawing does not exist but we're gonna go ahead and create it right now. So let me just go somewhere where we are not doing all of this pointer events, like resize selected layer. So let's add const, start drawing is gonna be use mutation. There we go, so that should now resolve the start drawing error from here and here we just have to align the props. So inside of start drawing here, we are going to go ahead and extract the following props.
Set my presence. Point, which is a type of point. From types canvas. And pressure, which is going to be a number. Like that.
Now let's go ahead inside of here and let's do set my presence pencil draft is going to be an array and then inside of it another array point dot x point dot y and pressure So make sure you do a matrix of arrays like this. And then let's add panColor to be last used color. And make sure you add last used color to the dependency array right here so now what we have to do is we have to revisit our LiveBlocks config so that we can extend it with the pencil draft and the pencil color. So let's head inside of our config. So that's going to be in the root here, liveblocks.config.ps.
And inside of our presence, Let's go ahead and add pencil draft to be an array X number as the first argument, Y number as the second argument and pressure number as the third argument and that itself is gonna be an array of arrays. So a matrix and null. And then color is going to be the type of color or null. I believe we already have color imported from types canvas. Great!
And now what I want to do is I also want to go inside of my reusable room component. So components room and let's pass in the pencil draft to be null and let's pass in the pen color to be null as well. And now if you revisit your canvas.psx again, there we go. Set my presence now throws no errors. Now the next part of the flow, after we call setDrawing which is initialized in onPointerDown, the next one is onPointerMove.
So let me just find where do I have on pointer move there we go on pointer move right here so we have the state for pressing selection net translating and resizing so after resizing Last one let's add else if canvas state mode sorry dot mode is identical to canvas mode dot pencil in that case we're gonna go ahead and create a method called continueDrawing() and we're gonna pass in the current and the event like this and let's also add the current drawing here and let me just continue drawing here And let me just also ensure that we added all the necessary elements here. So we should be having startMultiSelection inside of this dependency array. We should also be having the updateSelectionNet inside of this dependency array. Great. And now let's go ahead and let's actually create this method continue drawing.
So I'm going to do that right here above my continue start drawing. So const continue drawing is going to be use mutation again. There we go. So just by initializing this this should resolve that error here and here and now let's go ahead and let's destructure from here self and set myPresence and now let's get the point to be type of point and event to be react.pointerEvent now let's go inside of this method and we're gonna do extraction of pencil draft from self.presence and then we're gonna do if canvasState.mode is not canvasMode.pencil or if event.buttons is not equal to 1 or if pencilDraft is null but I'm using this equal sign I'm not using identical here keep that in mind and let's also return this and now let's go ahead and do set my presence and let's pass in the cursor to be point pencil draft to be pencil draft dot length is equal to one and pencil draft the first first array from the first array is equal to point.x and pencilDraft from the first array, the second element is point.y If that is true, we're going to add a ternary to use the pencil draft.
Otherwise, we're going to go ahead and create a matrix by spreading the existing pencil draft and then creating a new stroke using point dot X, point dot Y and E dot pressure. There we go. And make sure to pass in the canvas state.mode inside. Like this. There we go.
So now we have our continue drawing but there is one more event left. And that is inside of onPointerUp. So let's go ahead and find our onPointerUp event. And in here what we have to do is right after this first if clause let's intercept the inserting with the check for a pen so else if canvas state that mode is canvas mode pencil Open up this and then we're gonna chain this with the else event like that. Then we're gonna call insert path.
Without any props. And make sure that you pass in insert path here and let's see if we are missing anything here. So we should also pass in the set canvas state here. Looks like we are missing that. We have the history, we have the camera, we have insert layer, we have unselect layer so it's just set canvas state and insert path that we were missing.
So now let's go ahead and let's create this method to actually insert the path. So we have to go somewhere related to our drawings. Let's go ahead and find start drawing. Let's go ahead and write const insert path to be used mutation. So this will resolve those errors from on pointer up.
You should now normally be able to use those. And let's go ahead and extract the following from the first parameter, which is going to be storage, self and set my presence. And that's it. No other arguments here. Let's go ahead and call the live layers and use storage.getLayers here then let's go ahead and write const pencilDraft to be selfPresence And then let's check if PencilDraft is null.
Keep in mind I'm using the equal not the identical sign. PencilDraft.Length is smaller than 2. LiveLayers.Size overflows our MaxLayers constant which is 100. In any of those cases, set myPresence. PencilDraft is going to be reset to null and the function will be broken.
So if we broke any of those rules, we will not be able to insert our drawing. Other than that, let's create the ID using nano ID which we already have imported because we are using it in our insert layer method as you can see right here so we're going to do a similar thing here and before we can do the following thing we have to go inside of utils and we have to create a method called PenPointsToPathLayer. So let's go ahead inside of our util. Again, you can simply copy this directly from my source code if you're not into writing this. So let's export function PenPointsToPathLayer like this.
It's going to accept points which is a matrix of numbers whoops like that and a color which is a type of color and it's going to return a path layer which we need from types canvas so just make sure you import path layer from types canvas it looks like this and it has the points which is a number of, which is a matrix of numbers. All right, now inside of here, let's write if points.length is smaller than 2, we're going to go ahead and throw a new error, cannot transform points with less than two points. So we cannot work with that params. Other than that, let's define the left to be number.positiveInfinity, the top to be number.positiveInfinity as well, the right to be number negative infinity, and the bottom to be number negative infinity as well. And now let's iterate over our points.
For const point of points we're gonna go ahead and extract the x and y coordinates from a point from the individual point like that. If left is bigger than the X coordinate, in that case left is going to become the X coordinate. If top is bigger than Y coordinate, in that case our top value is going to become the Y coordinate. If right is smaller than our X coordinate, in that case, right will become the X coordinate. And if bottom is smaller than Y coordinate, in that case, bottom is going to become the Y coordinate.
And now let's go ahead and let's return our type to be layer type.path. Let me just see if I'm having any... So return is missing the following properties. Do I need to import? Oh, I need to import layer type.
Import layer type from types canvas here. And now we have to add X to be our left value. Y coordinates to be our top value. Width to be our difference between right and left and our height is going to be the difference between our bottom and top and fill is going to be our color and points is going to be points.map We're going to go ahead and destructure x, y and pressure from an array here. And we're going to return x minus left, y minus stop and pressure.
Like that. So let me see if I can collapse this like this so you can see it. There we go and you can see no more type errors here. So what we can do now is we can go back inside of the canvas here and we can wrap up our insert path function. So Basically what this util does is it uses the information we have from our drawing mechanism and it's going to transform that into something that we can render in an SVG element.
So I wish I could tell you more about this comparison and everything. If you want you can research it yourself, but it's basically a bunch of existing code that I found chat GPT Examples from live blocks and you know, just a bunch of trial and error so that's why I know this part seems a little bit tedious because I'm not because I'm just writing what I'm just saying what I'm writing but yeah it's the best I can do for now now let's go ahead and continue doing this by adding a live layers dot set and passing the ID and new live object inside is going to be pen points to path layer which you can now import from libutils because we just created that and inside pass in the pencil draft as the first argument and the last used color as the second argument. Like that. And then let's go ahead and write const liveLayerIds is going to be storage.getLayerIDs and let's go ahead and write liveLayerIDs.push and pass in the individual ID and then let's update our presence by resetting the pencil draft back to null and let's set our canvas state mode to be canvas mode.pencil so we can continue drawing by pressing down again.
And let's make sure to put last used color inside of our dependency array. So now what will happen is the following. So I'm going to prepare my terminal here. At least I think that's what will happen. If I try and draw something, once I release I'm going to get an error unknown layer type.
But as you can see this layer actually exists. So we did draw something we just have no idea how to render that. That's because we have to revisit our layer preview component and in there we have to create a new component called path. So let's go back inside of our layer preview component. And in here we're going to add a new case for layer type.path like this and this one should not have any semicolons and we are very simply going to return a new element called path so let's already prepare everything we're going to need key is going to be id points is going to be layer.points then we're going to have onPointerDown to be onLayerPointerDown and selectionColor is going to be our selectionColor like that and now let's go ahead and actually create our path.tsx component right here so I'm first going to install a package called perfect freehand so npm install perfect freehand which is going to be used for us to beautifully render our strokes.
So let me just close this. Can I please close this? Okay. Now inside of here, let's go ahead and import getStroke from PerfectFreeHand. And now what we have to do is create an interface path props with a number for the x-coordinate, for the y-coordinate, a matrix of numbers for the points, and fill is going to be a string on pointer down is going to be an optional void.
Let's pass in event to be react.pointerEvent and stroke is going to be an optional string. And now let's export const path component like this. Let's go ahead and destructure the props, path props which is gonna come to X, Y, points, fill, pointer down, on pointer down and stroke itself. And now we are simply going to return a path element for now like this. So enough for us to import this inside of the layer preview component.
So make sure you import this from .slash path and let's go ahead and see what error do we have here so let me go back inside of my layer preview here so we are not just passing onLayerPointer down instead in here we're gonna call the event and call onLayerPointer. Actually, do I need to do this like that? Let's do it like this. Let's pass in the ID here. Let's keep this to be onLayerPointer down, Like this.
Let's go ahead inside of here. Let's accept the ID to be a string. And let me open something for reference like a text. So let's just confirm. Yes, the ID is a string and then this will be on pointer down event.
Let me just change this. Oh, no, no, no, we can't use it like this. So no, no, no, let me refactor this. Let's go back inside of on pointer down, my apologies. So yeah, no need to add this ID and inside of your path, simply remove that ID from the path props.
So what we're gonna do here is open an event and pass in onLayerPtrDown and individually pass the event and then the ID. Like that. And the selection color, let's see what is wrong with the selection color. Selection color doesn't exist. Yes, it doesn't exist because I passed in the wrong attributes here.
So we are using stroke instead of selection color here. And we are also missing the X value to be layer.x and we are missing Y value to be layer.y. And we are missing the fill value to be layer.fill. And then we're gonna use color to CSS and pass in layer.fill or it's going to be in the default black color. So make sure you import color to CSS.
Okay, and now that we have that, we are still not done. So now what we have to do is we have to create a method which is going to calculate using this getStroke freehand here and that function is going to be called getSVGPathFromStroke and now I would highly highly recommend that you copy this from my github not because it's long but because I have no idea how to explain this function to you you're going to see what I'm talking about in a second. So this is the method which we have to create right now. I'm gonna paste it here. Not that one, sorry.
This one. GetSVGPathFromStroke. So I wish I could explain to you what's going on here but I have no idea. All I know is that it successfully transforms using the points that we have. So you can find this function, getSVGPathFromStroke either by pausing the video and writing it out yourself if you want to or you can go inside of myutils here and in here you have that method specifically getSVGPathFromStroke.
Great! So that will speed things up. Again it's not a long function it's just so confusing that honestly you're going to see where we use it. So we use it inside of our app folder, board, components. Where is it?
Our path right here. So in here, we're gonna use it in combination with our get stroke. So it's specifically tailored for this get stroke perfect freehand. So if you wanna explore it yourself, that's the direction you would go to. And now let's go ahead and do the following.
Let's give our path here a class name of DropShadowMD. Let's give it onPointerDown to be onPointerDown. And let's pass in the value to be getSVGPathFromStroke which you can now import from your libutils like this. So getSVGPathFromStroke and then inside of it you're going to fire your get stroke which you can import from Perfect Freehand like this. And inside of get stroke you're gonna pass in your points which you have from the props and then give it a size of 16, a thinning of 0.5, a smoothing of 0.5 and a streamline of 0.5 as well.
Now of course you can tweak these values until you get exactly what you want. But this one's working great for me. Now let's give it a style of transform. Translate. And let's go ahead and open X in pixels and Y in pixels.
The individual values of X and Y are going to be 0 as props. And fill is going to be fill. Stroke is going to be stroke. And stroke width is going to be 1. And I think that we can already try this out.
The only thing is we are not going to see it in real time. Only when we release our mouse button. So I'm going to draw an 8 here. And there we go. You can see how it appears but only when I release my key, right?
And I should be able to change the color normally. I should also be able to play around with layers. I could also use it in combination with something else. Great! So that is working But what I want to do is I want to enable real-time seeing of what I'm drawing because it doesn't make much sense otherwise.
So for that we're gonna go back inside of our CursorsPresence component. So let's find our CursorsPresence inside of Components. And in here I left a comment. To do draft a pencil. So that's what we're gonna do now.
So my apologies, just before we do that, this will actually work for when other people are drawing in real time. Right? So when another person is doing that. First, let's do it for ourselves inside of the canvas, which is going to be a little bit simpler than that. So what we have to do is go inside of the SVG right here and find the cursor presence and then let's go ahead and use our pencil draft.
Let me just see where we can extract our pencil draft from. So let's go all the way to the top of our application where we start with our live layer IDs. Right here where we have the layer IDs, the canvas state. And in here, let's go ahead and let's add our pencil draft to come from use self you can import this from live blocks config and in here we're going to use me dot presence dot pencil not pen color but pencil draft specifically like this, there we go. So now we're gonna go all the way to the bottom here where we started this.
If we have the pencil draft, we're gonna go, if pencil draft is not null but using equal sign not identical and if pencil draft dot length is larger than zero in that case we're going to start rendering our path component from dot slash path so the same path which we are reusing where is it? Which we are reusing for our layer preview component so that's the same component which we've just created and then we're just gonna have to pass all the necessary props so points are going to be pencil draft and our fill is going to be color to CSS from lib slash utils so make sure you add lib utils here and we're going to use the last used color and did I import color to CSS? I did not. So make sure you import color to CSS and passing the X to be 0 and Y to be 0 like this. And let me just see what did I break inside of my app so I think I broke something just a second I think I deleted some major key okay I'm just gonna go ahead and type it out again I think I'm selected something above and accidentally removed it.
So fill is gonna be get, sorry, color to CSS from Libutils and last used color. And X is gonna be zero and Y is going to be zero. Yeah, okay, it looks like I deleted somehow something from above previously. And I think we should already be seeing our own drawings here. So if I select a pen and start drawing, there we go.
You can see how this is now real time for us. But here's another user and another user will draw something now. You can see how it only appears after they leave the mouse. So now we have to do the same thing but for them to for us to see their drawing in real time. And for that we're going to go back inside of our cursors presence and we're going to go ahead and create something called drafts.
So let's go ahead and the same way we created cursors let's go ahead and create a const drafts. So drafts is gonna use others using use others mapped which you can import from live blocks dot config. So in use others mapped We're gonna go ahead and extract the other user and then we're gonna immediately return an object and write pencil draft to be other.presence.pencilDraft and penColor is gonna be other.presence.penColor. And we're gonna go ahead and use shallow here, which we can import from Liveblocks client. Let me just see that, There we go.
And let me just collapse this too. So they look nicer. Alright, so make sure you've added the shallow here. And then we are simply going to return a fragment. And others.map.
We get the structure of key and other. And then inside of here, let's go ahead and return. Let's check, sorry, if other pencil draft return our path, which you can import from .slash path. So let me just show you this .slash path. So the same component, which we've just used in the canvas and layer preview, pass in the key to be the key, pass in the x value to be 0, the y value to be 0, points to be other.pencilDraft, fill to be other.penColor?
Color to CSS from libutils, so just make sure you import this from here and pass in other pen color or use the default black color. Otherwise return null if we don't have other pencil.draft. And now what we have to do is render those drafts here. Like this. And let's try it out finally.
So I'm gonna go ahead and move my screen here a bit. And now if I start drawing something, there we go. We can now see the other user drawing in real time. Perfect! Amazing, amazing job.
You finished the entire project. There are just a couple of more hooks that I want to show you which can help you improve your project. So I want to show you a little hook that you can create. Basically what can happen is that if another user has a much larger screen than you and they move their cursor around it can happen that they trigger a scroll inside of your element here. So I couldn't really reproduce that here but it did happen to me during development.
And here's a useful hook that you can create to prevent that. So let's go inside of hooks and we're going to go ahead and create useDisableScrollBounds. So useDisableScrollBounds.ts. And inside of this method is going to be very simple we're going to import useEffect from React and then we're going to export const useDisableScrollBounds and we're going to call useEffect and very simply inside on the mount we're gonna call document.body.classList.add overflowHidden and overscroll-none And let's go ahead and return document body class list remove overflow hidden and over scroll none. So that happens on unmount.
Great. And now let's go and simply include this inside of our canvas.vsx component. So I'm gonna go all the way to the top here, Right here where we start with all of our hooks we can simply add another one. Use disable scroll bounce. You can do that from hooks right here and let me just move that with these elements here like that.
And here's another thing I want to show you and that is shortcuts. For example if you want to use ctrl Z or backspace to delete something we can do that as well. So let's just import use effect from react and let's go ahead and let's do it just above this so we're gonna call use effect here and we're gonna add delete layers and history inside of our dependency array. So make sure you do that before you add the delete layers. So how about actually do that just before our render function to ensure we have all of those functions from above, yeah.
Let me move it. Okay, here is the return function. So I'm gonna, this is where I'm gonna add it. And I still don't have delete layers. That is weird.
Oh, just a second. I forgot that we need to add delete layers because we don't use it here we use it in the selection tools but luckily that is a hook so all we have to do is add delete layers and do use delete layers from hooks use delete layers. There we go. So I'm gonna move that here as well now. Okay, so now we have delete layers and history.
And what we can do now is we can go ahead and write a function on key down. Event is going to be a type of keyboard event. And in here you can do a switch on the E.key. And for example, if case is Z you can go ahead and do the following check if E is control key or if E is meta key and if E is shift key in that case you can call history.redo else history.undo and for the other cases you can simply break sorry let's add break right here. Like that.
And let's go ahead now and simply add document add event listener and pass in key down and on key down. And always make sure that you unmount the event listener to prevent any overflows. So document remove event listener and pass in key down and on key down. Okay, so let me just try this out now. I will refresh this.
So now I should be able to use my shortcuts. So if I add a new sticky note here and change the color and I press Command Z, there we go. I can remove that. If I move this around, let's do a lot of changes. Let's go ahead and draw something.
If I do command Z, I remove that. If I do command shift Z, I bring it back. Perfect. And if you want to, you can do the same thing, for example, to remove an element using backspace. So for example, you can add a case to the backspace.
And then you can do delete layers and break. So for example, you can now select a sticky note and press delete, right? The issue with this is that if you're in the middle of typing something and press backspace, that's going to remove the entire element. So that's why I commented this out. You can probably fix this with event propagation or more strictly checking what's going on while you're writing but yeah just comment that out if you want to allow people to remove text while they're typing great but what I want to show you next is just that there are a couple of stuff which I've been told from the live blocks team are Deprecated and that should be our usage of to array.
I think there we go So we have two instances of to array and they've told me that we can do replace those with two immutable like this so let's find so the first one was I'm not sure where this last one was the immutable where did I use you now So I think I changed this one. Yes, 13 seconds ago inside of selection tools. So this is the move to front function here, I believe. Right? And the second one seems to be here in move to back so change those two to immutable because apparently the is the two array is deprecated so let me check if two array exists anywhere looks like it doesn't so The way we can test this out is by checking out if our layering works.
Oh, and it seems to not be working when I switch to immutable. Well, yeah, interestingly, it doesn't seem to work for me if I move it to immutable but regardless that's just a tip for you. If yours isn't working in the future it could be because you're using to array so it could be just the version of Live Blocks that I have. So let me go ahead and find my selection tools again here and see how we can refactor this back. So live layer IDs to array and the one in move to back is gonna go back to to array and let's see if that's going to work now.
So now, or maybe I'm just using a completely wrong way of testing. My apologies, let me please try it out like this. So this is working. Yes, it could be that I just did a completely wrong test here. So if I go back to two immutable, my apologies.
Yeah, I'm pretty sure it's gonna be visible now. Let's try it out. So if I try going back, yeah, it works completely with two immutable and you should be using two immutable. I just tested it literally on a black on black line so that's why it was not visible. Great so if you have any instance of converting to array you should refactor it to use to immutable like this.
There we go that is it you finished the entire entire project If you're interested in even more content on this project, you can visit the link in the description and purchase additional content on my platform where we are going to turn this into a software as a service and implement Stripe payments. Great, great job! Thank you so much for watching and see you in the next tutorial.