Disclaimer: I am not the author of svelte-cubed nor participate in designing the svelte-cubed component library. I'm just someone who love Svelte and love to talk about Svelte.
Here we are going to take a look how we can write a component library, like svelte-cubed, wrapping imperative API into a declarative component
00:00 Introduction 02:26 Imperative threejs code 03:53 svelte-cubed API 04:15 The Canvas component 12:55 The Mesh component 16:18 Using context 21:01 The Camera component 30:54 Let there be light 34:22 Refactor the context 46:13 The Group component 55:39 onFrame lifecycle 59:02 Summary
Reference https://github.com/Rich-Harris/svelte-cubed https://svelte-cubed.vercel.app/
🥰 Support me https://www.buymeacoffee.com/lihautan
Want to learn more from me? Check out these links:
Twitter: https://twitter.com/lihautan Blog: https://lihautan.com/ Newsletter: https://lihautan.com/newsletter
VS Code Theme: Dracula at night https://marketplace.visualstudio.com/items?itemName=bceskavich.theme-dracula-at-night ZSH Theme: bullet-train https://github.com/caiogondim/bullet-train.zsh
Hello. Hello, I'm Lehel. So today we're going to talk about Spell Cube. Spell Cube is a common library that uses three JS to let you build 3D scenes in Spell application. Let's watch. Rich Harris announced Speltcube in the recent Spelt Summit today I'm open sourcing the result of that work. It's a component library called Speltcube, and you can use it right now. There is a caveat. It is not feature complete. The documentation has gaps. Some things will definitely change. But I know you people well enough to know that that won't stop you. So before we start Disclaimer first, sorry about the clickbaity title, but I am not the author of Spellcube and I do not help design and implement Speltcube at all. I'm just someone who loves about Spelt and likes to talk about Spilt. So today we're going to talk about three GS and we're going to take a look at Spelt Cube and we're going to figure out how we can write a cell component using three GS. So if you take a look at the Shell cubed tutorials over here, you see that this is how you implement scene where you have a canvas, a mesh meshes, like an object with some geometry and a camera that looks at the 3D scene. So this is what you write in Spell Cube. But if you're going to write it in three JS, this is what you're going to write. Holy shit, there's so many lines of code. But how does Spellcube works? How does Spell Cube component libraries? How do we have all these components built out based on all these three JS codes right here? All the codes are imperative, meaning you have to line by line create each of the geometry and you have to edit one by one into the scene. Of course, you can see some similarities over here. You have the mesh and you have a mesh component over here. So how does this mesh component wraps around this mesh object? For instance, from tree. Js. That's what we're going to take a look today. So if you scroll all the way to the bottom following this tutorial, this is what you achieve with Felt Cubed. I'll try my best, but this is what I come up with, a three GS code which I wrote ahead of time to prepare for this. So let's take a look at the three GS code that I written. This is our component. I'm buying a container reference and we're going to render tree. Js into this container. So in the render scene, what you see here is that we create a scene, create camera, we create renderer and set up some of the things and then we start to add items, we start to create objects and add it to the scene. So you can see here this line scene. If I search this, you find that I have adding a few things to the scene. I add a box, add a group lighting. And then there's a grouping. Right. So here, you see, I create a group and I create like the floor and the grid which is these two things. Right. The floor and the grid which allows you to have the shadow. So I add these two into a group and add the group to the scene. So we're going to figure out how we can sort of transform all this. Make the group a component, make this three object a component, make the plane which is the floor component, as closely as possible as how Spelled Cube works. We will kind of look at this reference over here, look at the API, how we're going to use them and figure out what other things that we're going to want the props that we can pass in from there. Right, sounds good. Let's try and figure that out together. Right. So first of all, the scene is a component, is it? No. First of all, there's no scene component. There's one canvas the canvas. If you scroll all the way down, you see that the canvas is nowhere to be found over here. And also, if you look at here, there's no renderer, right. So in three GS, you need two things to start, like the whole scene. Right. First is the scene object where you will add items to the scene and you need a renderer to render the scene. So these two, I believe adds up to the canvas component. Right. So that's why we don't have anything. We don't have the component on canvas component. So we just wrap everything around this canvas component. So I guess that's what we're going to do. So first of all, let's create a folder and create our first component called the Converse. Okay. So here, let me try to side by side mode so that you can see our changes in real time. And what I'm going to do here is that I'll create another diff. We are going to import line break and we're going to it's called Canvas component. Right. So our canvas component over here, you see nothing yet. But in our canvas component, probably we're going to add a Div like this, right. So that we can render our triggers objects into it. So can I create this? Let's see, copy some of the things over here. Let's also create container. We don't have this yet. We have a style over here. So to let you see the conversion yourself, I'm going to add an online rate two PX solid, save this. So this is our canvas component. Right? This is what I have on top using imperative three DS code. The scene that we created here is a scene that we are going to create using start component. So Firstly, let's see what we have in the scene. We need to set up all this. Let's try to copy some of this code over here, background. Right. Hold on. I need to import three JS okay, so first is the background you look at here. Background is props for canvas. Okay. So let it be a prop. So export, let background. I'm going to set. So whenever background change, we're going to call scene. Background equals to background. Right. Every time when a scene changes, whenever background changes, we're going to change the background scene. And here I make it like a block, right? Yeah. I probably will have to add like some statement to redraw the scene, right. Every time back on change, I need to update the background and redraw the scene. I'm not sure how to do this yet, but this is what I'm going to do. So background. Okay, so here what we're going to do is we are going to pass in the background, right? So background, we're going to pass in a color called papaya. So this we need to import three JS as well. So don't worry that you are creating a new instance and pass it into the background over here. Since if this is not going to change, it's not reactive, this instance will only create once and pass it to spell only once. So if you look at the tutorial on top quickly come back again. You'll find that papaya. This is exactly how it's being shown in the tutorial, right? You add a background and color wipe exactly what we're doing over here. Just going to figure out how we can do that. Okay, so let's come back over here. Let's see what else we have. So we have this, we have the scene. Now we have come over here and create the renderer. So this I believe is also canvas. Do we have size, whatever? No, we don't have but I remember seeing it in the conversation. No, we don't have it and we don't have anything. Right. So probably we just let it be the container. We set a fixed height. So I believe here is that the parent conditions the dimension and the canvas just grow 100% into the container. Right. So that's why it does not need to pass in the width and height. But now we take it from the container. But we can't do it like this immediately because this container will only be available when we Mount this component. Right. So let's add on Mount, let's import on Mount from spell and let's create all this during on Mount, Mount. And let's move all that's going to set a variable called scene. In that case, I need to make sure that my scene is ready before I can set it the same time. I also need to set this over here. Well, that's quite annoying, but let's just let it be first. Okay, so to do duplicate code over here. Okay, so here we have our renderer, we have a scene. Okay, let's see what else we need. So we need to add the vendor element into the container and that's it, I guess. Let's take a look at what we have, right? We have a black empty scene over here. Yeah. So at least we have something now. So next thing is we're going to add some objects, right. And then we need to render. So probably we don't have any objects, we don't have any lights. So that's why you see everything is black over here. So we're going to create a few components. First is the camera, and then there's a mesh, right? So first, let's create that. So let's create mesh spilt and a camera. So this camera is called a perspective camera. So we're going to create perspective camera spell. And how we're going to use this is that we're gonna come over here in the canvas and we're gonna add the camera perspective camera. I'm going to add the mesh. And if you take a look over here, the tutorial, you see that it's exactly here, right. Mesh. So here, you pass in a geometry, this geometry. Let's copy from scene. This geometry is the box geometry. Right. Okay, so mesh box geometry. And then also, I believe they have a material of a tree mesh standard material. So this I have this over here. Let me just copy this material. Okay, so let's try to add these two in. So first is come over here. I'm going to create script and I'm going to export two props. One is the geometry and one is the material. Okay, so now pass it in. So we have to come over here and create this actual three GS match object. Right? So this is this mesh. So I'm going to create mesh, and let's see. And we have to add the mesh in sales into the scene. But how do we get the scene? How do we add it in? So first of all, there are two things we need to settle, right? So first is that if you console log over here and come over here and inspect, populates refresh, queue and refresh, let me Zoom it up a bit. Whereas that this match object is not being rendered at all. Why? Well, that's because the canvas does not render its children, although we add all this into the canvas under the canvas component. But this is supposed to render into the default slot, right? But the canvas itself does not have a default slot. So we're going to add that in. We're going to add slots in. So immediately when add slots, you see high from mesh. So now this is being rendered now. But now the second thing that we need to settle is how can I get this box and add it back to the scene? The scene is created in the canvas, and we have to somehow pass this thing down to the children, right? So passing from parent to children, what should we do? Well, we can use context. So let's create context. Maybe let's create a file called context. The reason I created this is that when we get a set context, we need to pass a key and that key itself can be a string or any object. Because the context itself is like a map. It can take in numbers, strings, objects and anything, right. So to make it unique and to make a context value key unique we can use like something like a symbol or object because object insert. Because when you create objects, when you pass objects into the map it's been compared by reference, right. So no two objects are the same unless they are referencing to the same object instance. So here I'm going to export, I'm just going to call the inverse context and I'm going to pass everything in, right? So this is the key and I'm going to see set contact, canvas and the context value, right. This is tricky. We pass the scene right now in it will be undefined and you can read it later on. So I probably will have to say maybe like an object like roots. And I'm going to create a property called scene. Again, if I do it like this right now it will be undefined. But later on we can set it as long as we read it. When it's being set then probably we are good, right. So we don't render the slot first until first thing. Let's rewind a bit. So let's come over here, canvas, roots, let's set this scene equals to the scene, right. So we have the contact set up correctly. Now if children read, let's try to read it, right. If get context. This is not if we read this now. Oh, it's already set up, right. So that's good for us. Let me see. I think this is because of heart reloading, right. So when you first Mount the whole thing, this canvas scene itself is undefined, right. So maybe one thing we can do is wait for this to be mounted. Wait for the scene to be ready. Only we render the children, right. So we can say if scene then. Okay, let's refresh again as soon as scene is ready. Only we render children. Now the scene is there so we can get this scene out of the scene equals to context. Hold on destruction. And we're going to add scene at box over here. Okay. So I believe it's added but because we don't have a camera yet, I believe. Let's try and see. Let's do the camera as well. So same thing, let's just copy the cover here. But instead of camera. Okay. Right. We need a camera instead. This copy over here. Okay, here, I believe camera, you have to set a position, right. I'm not sure what this is at all because it's a square scene. I'm not sure where the camera position. Okay, here, right. So we take in position as a props with an array and we can set it like this, right? So let's come over here, do it like this. Okay. So we have a props called position. And here we're going to see position and zero. One, two. Let's maybe have a default value of zero. And last but not least, we need camera to the scene. I deleted this. Get the scene from the context editor. Right. So let's take a look over here. Let's refresh. Nothing yet. Well, I'm not sure what I did wrong over here. I think we have everything. Right. So probably we just need to render the scene. Right. This one, we haven't rendered the camera. So camera itself. Okay. We need to look at a place. We don't really add camera to the scene. Well, I thought we need to add it to the scene, but we do not. So camera itself is not added to the we don't have to add a camera to the scene. But what we need is we need to call render to render the scene and the camera. So for camera, we need to do it slightly differently, right. We need to come over here and see add camera or something like that. Or maybe we just reset this camera, but we need to figure a way to tell the roots to do something. So maybe we can render it, right. So maybe we can come over here. We define a function called render. So here I'm going to say a camera is undefined and we have a function called render. And let's see what I'm going to do. We are going to call a renderer, and then so here we need access to renter. Or maybe we just have to render on. So maybe this is fine as well initially. And then here we can see canvas route. So what we can call renderer is the render scene with scene. So the scene is the canvas scene and this is the canvas. Okay, so we call this render function in our camera. Cool. Now, now we have blank background, but we can't see our Cube. Not sure where it's our Cube. So our Cube is the mesh. It has the geometry and material. I believe we pass it correctly, but it's nowhere to be found. Probably because we don't have a position for it. Let's see, where is the mesh? Do you have a position for box? Okay, maybe. Come on. Okay, we have that already. Interesting. I'm not sure what went wrong over here. Client high and width should be close to one. Okay, so this is correct. We have done this. Render size. Have we done that? Yeah, we have done that. Right. Probably here wants as we do this, we also need to call the render. Right. So now say root instead. Root seen at root. Render. No, nothing happened. Okay, so we have a geometry. Yeah, that's all we need, right. Maybe we don't have lights, but we've seen everything. Right. So without lights, it's probably going to be look like black Cube for now. Is our Cube the scene? Yeah, I'm not good at three GS at all. So you probably have to bear with me. Now, how will debug this? How do we know whether it's added to the scene correctly? Here? Render. Maybe we can figure out what is in the scene at this point when we turn render. Right. Okay, so we have background children, we have match. Okay, I think it's here, but position is probably we already see this. The object is already in the scene, but nowhere to be found. This is tricky. Let's try and figure out what here. Context exactly the same. Is it this one? No, everything exactly the same. Canvas, context, camera, random position of the camera. Okay, so position of the camera. Right. So here we need to set the position camera position instead. Because we are looking at. We place our camera at probably we have to pull our camera back a bit. And the position of camera is one, one, three. So going to copy that save. Yeah. So now we are at the position one, one, three. And looking at the Cube itself. Because Cube is looking at. And if a camera is there, it's not looking at anything, right. It's looking at itself. So now we have our camera, we have our mesh. Now we can add lights. Same thing goes, right? So I'm going to quickly go through this. We're going to create two lights, object, the ambient light and directional light. So it's same thing, MB. And then we're going to create the actual light. Not spelled. And I guess the quote is pretty much the same, right? So this looks something like this. Let's see the quote from the mesh. So you add to the scene, right? So it's the same thing with scene. And then same thing goes with the. So this is something that we can compare, right? So if you look over here, if you look at the lights, this is the default color of the lights. And this is the intensity of the light. So spotlight color equals this spot intensity. So we can come over here to the Kieran. Light intensity goes to 0.6, and the actual light is the same thing. So here, I'm going to copy this, copy the code from mesh. And here, I'm going to replace this light. Right. Light scene. So this is the intensity, intensity. This is the color. And this is the position. Right? So position. Okay, so here. Now you see something. It looks weird. Let's see what goes wrong again. Try to refresh this again. So the action line is added, right? It's like this, right? So we have almost everything in here except that, you know. So now slowly, you realize that there's code that is kind of duplicated in all our components. Right? Here we add scene, and then we call render. Here we add actually, I'm not sure about the render. Let's try to remove this. Yeah, you have to call render. Right? So we add the item and then we render. So here the Get context, add the item to the scene and call the render. Again, this is kind of like copy pasted over several components, right. This creating the new three GS component is something that is unique to the component but this is something that we can refactor. So this is something we are trying to refactor this out the Get context, adding the item to the scene and call the render. So how do we reduce this? Get context code. So get context is just like a normal function. You can call it here or you can call it somewhere else. But the key is you have to call get contact in the component initialization, right. So you probably have seen errors where if you try to call maybe like some callback here, right? If you call get contacts later on when you do like a on click or something, call back over here. You probably can get an error because you'll see that the Get context should only be called during component initialization which is here as long as it's not inside any function that will be called later on or unmount callbacks and things like that. You can use get contact, right. And you can have a function like this. See set up and call get contacts here and then we call the setup function during component initialization. The key is to call this during component initialization. Then it's okay to get context inner function. So here, this is what we're going to refactor to, right? So we can take in this thing and then return us light, right? If we do it this way then this is the then we can refactor all this code in here the root equals to the get context roots seen at the item. Right. So this is the item item and call render and then return the item. We haven't used light yet, but actually this is what you probably would need to do, right. So if you have a way to change intensity. So maybe let's try to create an input that we can change intensity. So input type range min equals to zero Max equals one bind value equals to intensity. Maybe step equals to one and let's intensity. I'm going to pass intensity over here for our directional light. Okay, default is so here, if we change this, nothing happens, right? What you need is we need to come over here and say light start intensity goes intensity. Let's do this again. Nothing happens. Oh wait, so we probably have to call root render, right. Which means we need to return both of them root and light. So here I'm going to say root rent. Okay, let's try and see. Okay. I need to print out intensity somewhere so inspect console cannot set properties of undefined light. Oh, okay. Ambient light. This is written as item light. Try again. So you can see now it's getting lighter and darker, right. With the changing density. If you don't have root render refresh. This changing, it does not work, right. Every time you change something in the scene, you have to re render it again. That's why we call this render. So now you can see changing intensity and ambient lights. Leave it to this over here. Probably we can have this function extracted out for other components. Right. So here I'm going to export this setup function. Export setup. Nothing special, right? Everything we need is here except we need imports. Okay. So we can import set up from the context and recall set up over here like this. So it still works, right? So next thing is we're going to come over here to the mesh. No, let's try the actual first. So here, set up instead and set up the three directional, right. So it returns an object of roots and the item which is in our case the directional light. So now we can also come over here and see if the directional like intensity equals intensity. So now our fractional changes as well. Maybe you have two different intensity. Okay, so this one, something goes wrong. Intensity equals in. All right, so now this one changed the direction like intensity, but it's not working. Let's see, no error refresh again, maybe you can't tell it is changing, but it's not affecting this scene as much at all. It's not affecting the scene at all. Why? That's interesting. I'm not quite very sure. We do trigger the rerender with the latest value of this. Okay, so if we do not have directional light, then we are not. Everything is just like lights. There's no one light cast on one direction. So that's interest. That's weird. Maybe not set it this way. I'm not quite sure though. I'm not really sure about how. Wait, I know why we have a pipeline. Okay, let's close this. Right. So now it's getting darker and wider as we change the intensity. Probably. Wait, so here we don't need this anymore, right. Because it's already in the setup. This we don't need anymore. And here we can actually don't need this if we change this refresh, because this will still be evaluated whenever it actually changed on the first. When you first mounted item, we will call again. Right? Even if you have here and here, this will be called again. So we don't need this. And same thing. We can actually do a lot of similar things over here. Can move all this in here. So if we have something like say, let's see position of the light. Right. So if instead we have position, let's see, position is maybe we have a negative 5%. If you have this, come over here and see the lights moving from one lecture to another. Right. So this is how we can set this up. Reactive felt Cube components. Right? So what we're going to do next is basically redo the whole thing again for mesh and production light. So I'm not going to do that right now, but we're going to look at one interesting place, right. It's the group. So if you create a group component, whatever objects that you add is add to the group instead of adding directly to the scene. Right. So you're not actually adding to the we should not actually add to the scene. When we do set up, it's actually adding to sort of like a parent component. I'm not sure you can get what I'm trying to stay here, but if you look at the example of the group group, you see that. Now, later on we will create a group group spelled you can come over here and create a group component. And inside group component, you create another match component. So the match is added to the group and group is added canvas. Right. So image is at the canvas. So they're actually adding to the parent rather than getting the root scene and added to that root scene. So this is something we're going to trick a bit. So how do you keep getting parents? Right? So you can get a lot of parents over here. You can group a lot of ways of grouping things. So how do you create this kind of grouping? How do you get like parents? Right. So again, we're going to use context. What it works is that we have another context called the parent context. And every time when you create a new object over here, each of these will have to overwrite the parent context. So when you look at context, when you get the context value, you look for the value from the nearest parent, right. So if here sets up a context, here, sets up a context, here sets up another context. When mesh is reading that context, read from Y because this is their nearest parent and Y is trying to read from context reads from his nearest parents, which is X, X reads from his nearest parents, which is canvas. So that is what I'm going to do. Apparent context. So here, let's just copy some of the mesh code over here. I think I already have it here. The mesh is this, right? The geometry and the material is like this geometry material. Copy this too. So we are going to create the group component. So the group component, again, we're going to copy everything we have from the mesh. Okay. I think we copied from ambient because we have a set up here. Right. So set up here, I'm going to create a new group, which is a group that's all create a new group. Yeah. For now, we don't do anything. Just set up like a new group. Okay. So we have a new group. So when we do set up here, we need another context, right? We say we need a parent context to kind of transparent new context. So here, when we do set up, we need to get the parent. Right. We need the root. We also need a parent. So we need a parent because root is where we can call the render. We can trigger a render of the whole thing. So parent we're going to say first is we can get context of the if you are the root, you don't have any parents, then probably we need to get the root scene, right. If the parent is not available, then we get the seat we add to the seat. If the parents are available then we can use the parent seat. Need to add it in here. At the same time we also need to set context, set a new parent. The new parent is actually yourself, right. Because the group right. So here if you come over here now let's see, we probably have already added a new group and the new match. The only thing is we need to set the position of the group. The group position is this position. So I'm just being lazy here. But you should take this from the props, right. I believe you can do it yourself now. So just go and figure it yourself. So here we have the group, we have the mesh is still not added. So the group itself can render children. So in slots, right. And then the mesh itself is still added to the roots scene, right. So now we need to do the setup set up and here we're going to set up mesh. So here we have root and the item is the box. This we don't need, we don't need all this. Refresh this. Now I believe it's added to the group already, right. So if we come over here and we take a look the scene itself, if we come over here and console, let's take a look at our latest scene. We have matched light group and then for group we have children which is the mesh, right? That's good. That's great. So we have everything. Now the thing is this needs some rotation if I remember right, the whole this needs some rotation. So here I'm going to add the it's going to do it directly, right. This is the floor instead. This is the box. Can I do anything? I need to set rotation. Let's rotation and receive shadow, right. Export Let receive shadow, shadow equals false select on the box will cast a shadow and the floor will receive a shadow. So here I need to same thing, I need to come over here and say the box rotation is rotation, zero rotation, one this receive shadow and car shadow. Okay. And then every time I change, okay, so now we set things up. Let's come over here and let's set the rotation. This is going to be this and receive shadow is true. And on the other hand this is going to be casting shadow, right. Of course we need to set the whole scene to be able to have shadow and stuff that can be controlled through props. But I'm just going to be lazy. You can figure that out yourself. I'm just going to come up here and set everything up. Now you see a shadow being casted over here, right. So last thing you can see that when we do rotation, finishing touches rotation, right. There's an SC on Frame Cube On Frame that changes the rotation of the object. Right. So we can have a spin over here like this. We can set rotation already because we already takes in that props. And if we change this to the spin, you'll notice that now we can spin our object already but we need to do it on frame. And On Frame is actually a request Animation frame. We do it on the next frame. And when you use Request Animation frame you need to take care of a few things. First is you need to set up and then you need to make sure to remember to clean it up, right. You can use the on frame anywhere in any component. Whenever the component is being removed, you need to remember to remove it, right. So we can use the On Mount from spelled. So when you use On Mount in here, make sure that On Frame has to be called during component initialization because On Mount itself has to be called during component initialization. But the good thing is that once you set this up like this you can return a cleanup function and whenever the component is being cleaned up removed, this callback function will be called and will clean up for you. Right. So here we're going to cancel Animation Frame call. So we're going to create a function frame callback because this we're going to do it on a loop, right. So here we're going to call the callback and we're going to request a new shift frame again equals request. And here we're going to see the frame callback is this, right? And now we can use our On Frame on Frame on Frame spin plus equal zero point save this come back and let's take a look. Spinning and because of binding this is moving as well. But as you can see here we have On Frame setting up and if this component is being unmounted you are pretty sure that this will be canceled, right? So that's all of this video, we've spent quite a long time talking through all the different bits and pieces. But let's do some summary over here. First of all, we learn a few things on how first is we learn how we can convert an imperative code like this to a declarative one on how we can create a wrap this to a component. Right. Secondly, we learn about how we can refactor context. Here we notice that every component, every component we have to get the context and set things up. We can refactor that out into a function as well. As this setup function is being called during component initialization and we learn how in context. We also learn how to get the latest parents by getting the parents and then reset and override the parent right get and set context. We do that last but not the least we learn about using same thing of refactoring the lifecycle. We create a new lifecycle called on frame has to be called during componentialization and it does request animation frame right. So that's all for this video. If you like this kind of video smash the thumbs up button and comment what you want to look at next be sure to subscribe to my channel. I hope you have enjoyed and see you in the next video. See ya bye.