This is so good. Your code is very clear and easy to adapt. I like to start all the particles in a small space in the centre of the screen for a "big bang" moment.
Thank you! That's a great suggestion about having a big bang moment. I get a bit of the same effect by changing the rules in the middle - particles that were attracted to each other suddenly explode outwards. But I haven't done it with all of the particles at once.
Pretty amazing to see this kind of organized behavior stemming from simple initial parameters. Have you thought about goal-oriented learning problem that uses some kind of evolutionary algorithm (including a neural network, but not necessarily) that produces those parameters?
The variety and detail of the emergent behavior is really amazing. I do have an idea for an evolutionary algorithm approach where the forces and distances are evolved. It creates an even larger parameter space and makes things run even slower. But I'll try to get a version of it posted in a week or so.
I haven't finished the video yet but I'm very happy to see you tackling this coding challenge. I've already written my version of particle life and am curious how you wrote yours. Now, on to the video.
If you spot any interesting differences in the approach let me know. One thing I didn't focus on was efficiency. There are probably some good short cuts to avoid having to do quite so many distance calculations. (Short of creating a complete quad tree, which should speed things up a lot.)
@@programmingchaos8957 Your code is very elegant, mine isn't. I wrote my code with a couple of differences. 1) I used the standard formula for attraction and repulsion. 2) I worked with arrays for position and velocity in stead of a class. 3) First I made an array of all the couples and iterated over the couples. That way the distance between two particles only has to be calculated once. 4) I did not manage to create a toroidal field so the particles are steered away from the edges. Your solution is more beautiful. 5) On my system your code runs about 4 fps with 4000 particles. My code does about 34 fps. I don't understand what accounts for that big difference. 6) I managed to let the program run over 16 cores speeding it up considerably. If you are interested I can try to do the same with your version. On 16 cores my code gets a 134 FPS. A quad tree would definitely speed things up but I don't know how to do that. I would be very much interested in that. I learned a lot of your tutorial. Thanks for that. Cheers, Adriaan.
I spent a lot of time trying to simplify the code to make it more explainable, but not so much to make it run faster. I suspect using arrays instead of classes and creating couples speeds things up quite a bit. And having to do do four extra checks to make the screen toroidal also slows my code down. The other thing I noticed was a what seemed to be a big drop in performance for me between about 1500 and 3000 particles. I suspect it has to do with how many particles can fit in faster cache memory. Another reason why simple arrays, rather than classes of PVectors may be much faster than expected - the arrays all fit in cache whereas the objects containing PVectors (which for example have extra space for a Z variable I'm not using) don't. A multicore version would be very cool.
In order to be able to calculate many particles and the cells they form some kind of calculation simplifying process is needed, for instance one that measure the attractors and how close they are to a state of equilibrium. When they are within some range of an equilibrium there boundary interaction is calculated to determine the min force needed to break the equilibrium of the specific cell. once that has been determined the particles within that boundary get less often updated unless the boundary margin is surpassed. When that function gets stacked with scale internal structures become more and more stable and will cost less processing power over time.
That's a great idea! I suspect it would have been a bit long to put in the video, but would make an interesting follow-up. Do you have any links to examples of exactly how this is done? I suspect that there are some fairly well studied guidelines for how close to equilibrium is close enough, etc. and wouldn't want to try to recreate that work in my own.
For optimization you can use a quad tree or chunk your environment into regions so you don't have to have an O(n^2) loop where you check every particle against every particle. Instead, you can compare near particles with near particles.
I can't figure out the PVector direction in update(). It is 0 but you still do a direction.mult(0) ? Is that to reset position in each loop ? For now I'm just trying attraction / repulsion based on mass. Thanks
Yes! The first time through the loop its already set to 0,0 by the constructor, but you need to reset it to 0, using the .mult(0), for all of the following loops.
@@programmingchaos8957 But the constructor does not have this PVector , does it ? Or is it also called constructor when you declare those variables inside void update() ? Thank you.
@@limboreversible I'm not positive I understand the question. But every time update() is called a new PVector called direction is created (it's deleted at the end of the update() function so we're not wasting memory). In the line: Pvector direction = new PVector(0,0); the part that is PVector(0,0) is calling the constructor (constructors always have the same name as the class, in this case PVector). And is setting the values to 0,0. (It's likely that the constructor does this automatically, but I didn't double-check the documentation and feel more comfortable doing it myself so I'm sure it's happening.) The line direction.mult(0); resets the direction to 0,0 for each new loop. In reviewing the code it's not strictly necessary to use 0,0 in the constructor, the direction.mult(0) will set it to 0,0 for us. But it also doesn't hurt. Hope this helps.
It's because this code is slightly above my knowledge. I've been playing around with it for some time now but instead of the "tables" , I'm trying , inside update(), something like "if (dis < something) {apply repulsion force} , if (dis > something) {apply attraction force}. Reaching some interesting results but not spectacular, and it all feels slow. THANK you for your explanation@@programmingchaos8957
I would be greatly interested in a follow up in which you make the code run faster. I'm thinking about a grid partitioning system or quadtree. Thanks for the tutorial so far.
I think it really needs a quadtree, but instead I've gotten side-tracked on an evolutionary version where the forces and distances evolve. I'm going to try to get that posted first.
this is good , but one thing i didnt understand at all is what lang did you use , and what ide did you use , could you tell me tthis , and also what books did you read to have this knowledge , i am too interested in this kind of knowledge , please tell me , Thanks;
I'm glad you enjoyed it. The language is Java using the processing IDE. You can download it for free from processing.org. It takes about 3 minutes to download and unzip - very easy installation. For particle life I mostly used resources from the web. But my undergraduate degree is in physics, which helped a lot for understanding the forces and and vectors.
It looks very weird to see you put a for each loop for "particles" list variable within the particle class at around 10:27, when the original declaration was in the main class. alot of the syntaxx doesn't look very java to me.
Good point. I think it's a question of perspective. One option is each particle calculates the forces being applied to it and moves accordingly (this is how I programmed it). The other option is to think of the forces as being applied globally and have the main class calculate the force on each particle and apply it to the particle (which I think is what you are suggesting). The main class approach you are suggesting arguably makes more sense from the perspective of 'the forces are part of the universe/main class' and so are applied to the particle by the main class. On the other hand if the particles do the calculations within their class the code is much more parallelizable (which admittedly I'm not taking advantage of). One downside to Processing is the way setup() and draw() work encourages the use of global variables, like the swarm in this example, which leads to less-Java like code.
Subscribed! There are some great topics there I need to watch. And I really liked the book list, I need to check some of those out as well. Not to mention you also have chaos in your title :)
@@programmingchaos8957 if two particles get close enough, then one/both of them changes colour (depending on predetermined rules), haven't really played around with it for long enough for it to produce something interesting but maybe you could simulate real-world reactions between elements
This is amazing! It is crazy that with just a few rules you can create such complexity. I made a p5js version but I can't share links here. Anyway, thanks for sharing the processing code.
Thank you! I'd like to see the p5js version. I'm not sure if you can send the link as text (something like domain_subdomain_project - basically replacing slashes with underscores). I find all kinds of emergent behavior fascinating, but this is one of the most impressive. In case you missed it I also have videos on a 3D version (surprisingly easy to code with Processing). And an evolutionary version: clumps of particles represent organisms that share the same rules. If they collect enough food they reproduce forming new clumps with slightly mutated rules. Eventually they evolve into 'organisms' that can efficiently seek food.
@@programmingchaos8957 Let's see if this works. I am trying for the fifth time :) openprocessing_org_sketch_2312160 Note: The first underscore is a dot. I will check the 3d version. I still have a lot to learn :) Thanks!
@@programmingchaos8957 I could not add the link here, so I just made a short animation and uploaded it in my channel. You can check the links there. Now to study and understand the 3d version. Thanks!
Just knowing the sketch number at openprocessing.org made it easy to find. Very nice project. I also subscribed to your channel earlier today, so I'll see the video there. I haven't watched a lot of the videos yet, but am going to have to check out geogabra. I saw both your trees and a Lorenz attractor, projects that I've done in Processing.
Processing, which is a free environment for Java (or JavaScript or Python). You can download it from processing.org. Takes almost no time to install and is nice for these quick(ish) programs.
I would LOVE to see a data oriented approach to this, maybe with some kind of ECS implementation. Dont get me wrong I love me some OOP but this feels like such a classic use case for DOP.
That's a great idea! For now I'm sticking with OOP because it's more straightforward in Java, but if you implement it with ECS let me know how it goes.
Please let me know how it goes and if you have any different tactics. For finding the nearest particle, with wrapping, I'm pretty sure there's a clever mathematical trick to make the code more compact, but I couldn't find one.
@@programmingchaos8957 would that make it faster because the main issue with my current situation is the speed. Im getting around 5fps and im running it on a fairly good computer
If there were a way to combine both checks for width into one check and both checks for height into one check that could speed things up a bit - there's not a lot of code in the update loop so every instruction matters. I saw a big drop in performance between about 1500 and 3000 particles. Which I think implies that somewhere in that range the array don't fit in cache memory anymore, which slows things down a lot. You might see a big performance increase at some critical particle number. Another thought, I haven't experimented with it, but my approach of wrapping the particles could be slowing things down a bit. It requires 2 modulus (%) operations everytime, even when the particles don't need to wrap. A simple if might(?) be faster.
Lol so many neat math coding tricks in a vid. Awesome. Also to deal with the parts movving too fast per frame, you could use a smaller K and calling update many times per frame. In game engines would be called physics substepping.
Thanks! Substepping is a great suggestion. Of course, at other time they move too slow (at least given how impatient I get) so making it adaptive would be ideal.
Glad you liked it! Let me know if you see any obvious tweaks I should consder. I know there are some changes that would improve efficiency, I was aiming at simply code. I'm not sure what's up with links, I'll double-check the channel settings in case I have something set wrong.
@@programmingchaos8957k Haven't looked at efficiency, as it seems to run fine on my computer, full screen with 1300 particles. I added the mouse as a type of its own, with a bigger mass - that was fun. I have setParameters() being triggered every 10-20 seconds at random. Each particle has its own random saturation and radius, so this looks nice: fill(type * colourStep, saturation, 40); circle(position.x, position.y, radius); fill(type * colourStep, saturation, 100); circle(position.x, position.y, radius/2);
Somewhere between 1500 and 3000 particles I see a big drop off. The mouse approach sounds great. In the previous video I showed a model in which the particles only respond to the mouse, which makes it possible to create waves of particle motions. I haven't experimented with saturation or radius - that does sound nice. Good color choices is not my strong suit.
@@programmingchaos8957 The complexity is O(n^2) I guess. Short of a quadtree, the biggest efficiency gain would be avoiding sqrt() calls in mag and normalize. That is, storing squared minDistances and radii values and doing the sum of squared differences of position
It's Java, but using the processing interface, which hides a lot of the Java syntax overhead and includes the easy to use graphical libraries. Processing is free to download from processing.org. I do a lot of more 'serious' programming in C++ and C#, but I really like Processing for smaller projects and testing ideas.
I totally missed that, very cool. Thanks for pointing it out. I've been trying to figure out a way to 'rewind' the results so that when an interesting structure appears its possible to know not only the rules, but also the initial configuration of particles that led to it.
It does resemble the organisms she studied. If you're interested in the topic apparently some scientists believe they recently discovered a newly evolved example of a symbiotic organism becoming an organelle: www.newscientist.com/article/2426468-a-bacterium-has-evolved-into-a-new-cellular-structure-inside-algae
Thank you! Also, if you haven't already you may want to check out the 3D version ua-cam.com/video/G25S5FaMhH8/v-deo.html or the evolving 2D version: ua-cam.com/video/Nor4FxoLT9U/v-deo.html
This is so good. Your code is very clear and easy to adapt. I like to start all the particles in a small space in the centre of the screen for a "big bang" moment.
Thank you! That's a great suggestion about having a big bang moment. I get a bit of the same effect by changing the rules in the middle - particles that were attracted to each other suddenly explode outwards. But I haven't done it with all of the particles at once.
Pretty amazing to see this kind of organized behavior stemming from simple initial parameters. Have you thought about goal-oriented learning problem that uses some kind of evolutionary algorithm (including a neural network, but not necessarily) that produces those parameters?
The variety and detail of the emergent behavior is really amazing. I do have an idea for an evolutionary algorithm approach where the forces and distances are evolved. It creates an even larger parameter space and makes things run even slower. But I'll try to get a version of it posted in a week or so.
I haven't finished the video yet but I'm very happy to see you tackling this coding challenge. I've already written my version of particle life and am curious how you wrote yours. Now, on to the video.
If you spot any interesting differences in the approach let me know. One thing I didn't focus on was efficiency. There are probably some good short cuts to avoid having to do quite so many distance calculations. (Short of creating a complete quad tree, which should speed things up a lot.)
@@programmingchaos8957 Your code is very elegant, mine isn't. I wrote my code with a couple of differences. 1) I used the standard formula for attraction and repulsion. 2) I worked with arrays for position and velocity in stead of a class. 3) First I made an array of all the couples and iterated over the couples. That way the distance between two particles only has to be calculated once. 4) I did not manage to create a toroidal field so the particles are steered away from the edges. Your solution is more beautiful. 5) On my system your code runs about 4 fps with 4000 particles. My code does about 34 fps. I don't understand what accounts for that big difference. 6) I managed to let the program run over 16 cores speeding it up considerably. If you are interested I can try to do the same with your version. On 16 cores my code gets a 134 FPS. A quad tree would definitely speed things up but I don't know how to do that. I would be very much interested in that. I learned a lot of your tutorial. Thanks for that. Cheers, Adriaan.
I rewrote your code so that it runs using 8 threads. Now with 4000 particles in stead of 5 FPS I get 32 FPS. Let me know if you are interested.
I spent a lot of time trying to simplify the code to make it more explainable, but not so much to make it run faster. I suspect using arrays instead of classes and creating couples speeds things up quite a bit. And having to do do four extra checks to make the screen toroidal also slows my code down. The other thing I noticed was a what seemed to be a big drop in performance for me between about 1500 and 3000 particles. I suspect it has to do with how many particles can fit in faster cache memory. Another reason why simple arrays, rather than classes of PVectors may be much faster than expected - the arrays all fit in cache whereas the objects containing PVectors (which for example have extra space for a Z variable I'm not using) don't. A multicore version would be very cool.
In order to be able to calculate many particles and the cells they form some kind of calculation simplifying process is needed, for instance one that measure the attractors and how close they are to a state of equilibrium. When they are within some range of an equilibrium there boundary interaction is calculated to determine the min force needed to break the equilibrium of the specific cell. once that has been determined the particles within that boundary get less often updated unless the boundary margin is surpassed. When that function gets stacked with scale internal structures become more and more stable and will cost less processing power over time.
That's a great idea! I suspect it would have been a bit long to put in the video, but would make an interesting follow-up. Do you have any links to examples of exactly how this is done? I suspect that there are some fairly well studied guidelines for how close to equilibrium is close enough, etc. and wouldn't want to try to recreate that work in my own.
thank you so much man, i was trying to program this myself and you gave me so much insight
I'm glad it was helpful!
For optimization you can use a quad tree or chunk your environment into regions so you don't have to have an O(n^2) loop where you check every particle against every particle. Instead, you can compare near particles with near particles.
Absolutely right! That was more than I wanted to squeeze into a ~30 minute video though. I'm thinking about a separate one just on quadtrees.
I can't figure out the PVector direction in update(). It is 0 but you still do a direction.mult(0) ? Is that to reset position in each loop ? For now I'm just trying attraction / repulsion based on mass.
Thanks
Yes! The first time through the loop its already set to 0,0 by the constructor, but you need to reset it to 0, using the .mult(0), for all of the following loops.
@@programmingchaos8957 But the constructor does not have this PVector , does it ? Or is it also called constructor when you declare those variables inside void update() ? Thank you.
@@limboreversible I'm not positive I understand the question. But every time update() is called a new PVector called direction is created (it's deleted at the end of the update() function so we're not wasting memory). In the line:
Pvector direction = new PVector(0,0);
the part that is PVector(0,0) is calling the constructor (constructors always have the same name as the class, in this case PVector). And is setting the values to 0,0. (It's likely that the constructor does this automatically, but I didn't double-check the documentation and feel more comfortable doing it myself so I'm sure it's happening.)
The line direction.mult(0); resets the direction to 0,0 for each new loop.
In reviewing the code it's not strictly necessary to use 0,0 in the constructor, the direction.mult(0) will set it to 0,0 for us. But it also doesn't hurt. Hope this helps.
It's because this code is slightly above my knowledge. I've been playing around with it for some time now but instead of the "tables" , I'm trying , inside update(), something like "if (dis < something) {apply repulsion force} , if (dis > something) {apply attraction force}. Reaching some interesting results but not spectacular, and it all feels slow. THANK you for your explanation@@programmingchaos8957
I would be greatly interested in a follow up in which you make the code run faster. I'm thinking about a grid partitioning system or quadtree. Thanks for the tutorial so far.
I think it really needs a quadtree, but instead I've gotten side-tracked on an evolutionary version where the forces and distances evolve. I'm going to try to get that posted first.
Wow, can't wait to see that tutorial. They really help me hone my skills.@@programmingchaos8957
this is good , but one thing i didnt understand at all is what lang did you use , and what ide did you use , could you tell me tthis , and also what books did you read to have this knowledge , i am too interested in this kind of knowledge , please tell me , Thanks;
I'm glad you enjoyed it. The language is Java using the processing IDE. You can download it for free from processing.org. It takes about 3 minutes to download and unzip - very easy installation. For particle life I mostly used resources from the web. But my undergraduate degree is in physics, which helped a lot for understanding the forces and and vectors.
It looks very weird to see you put a for each loop for "particles" list variable within the particle class at around 10:27, when the original declaration was in the main class. alot of the syntaxx doesn't look very java to me.
Good point. I think it's a question of perspective. One option is each particle calculates the forces being applied to it and moves accordingly (this is how I programmed it). The other option is to think of the forces as being applied globally and have the main class calculate the force on each particle and apply it to the particle (which I think is what you are suggesting). The main class approach you are suggesting arguably makes more sense from the perspective of 'the forces are part of the universe/main class' and so are applied to the particle by the main class. On the other hand if the particles do the calculations within their class the code is much more parallelizable (which admittedly I'm not taking advantage of).
One downside to Processing is the way setup() and draw() work encourages the use of global variables, like the swarm in this example, which leads to less-Java like code.
I converted the code to python, then made several changes to speed it up. I also added double buffering. My video is on my channel.
Subscribed! There are some great topics there I need to watch. And I really liked the book list, I need to check some of those out as well. Not to mention you also have chaos in your title :)
Great video. Very helpful.
Thank you!
Thank you, amazing tutorial, worked for me :)
experimenting with making the particles undergo "chemical reactions" if they get close enough
Your very welcome! I'm always happy to hear that these are helpful. What sorts of chemical reactions are you trying?
@@programmingchaos8957 if two particles get close enough, then one/both of them changes colour (depending on predetermined rules), haven't really played around with it for long enough for it to produce something interesting but maybe you could simulate real-world reactions between elements
if two particles get close enough, one/both of them changes colour (based on a set of predetermined rules)
This is amazing! It is crazy that with just a few rules you can create such complexity. I made a p5js version but I can't share links here. Anyway, thanks for sharing the processing code.
Thank you!
I'd like to see the p5js version. I'm not sure if you can send the link as text (something like domain_subdomain_project - basically replacing slashes with underscores).
I find all kinds of emergent behavior fascinating, but this is one of the most impressive.
In case you missed it I also have videos on a 3D version (surprisingly easy to code with Processing). And an evolutionary version: clumps of particles represent organisms that share the same rules. If they collect enough food they reproduce forming new clumps with slightly mutated rules. Eventually they evolve into 'organisms' that can efficiently seek food.
@@programmingchaos8957 Let's see if this works. I am trying for the fifth time :)
openprocessing_org_sketch_2312160
Note: The first underscore is a dot.
I will check the 3d version. I still have a lot to learn :) Thanks!
@@programmingchaos8957 I could not add the link here, so I just made a short animation and uploaded it in my channel. You can check the links there. Now to study and understand the 3d version. Thanks!
Just knowing the sketch number at openprocessing.org made it easy to find. Very nice project. I also subscribed to your channel earlier today, so I'll see the video there. I haven't watched a lot of the videos yet, but am going to have to check out geogabra. I saw both your trees and a Lorenz attractor, projects that I've done in Processing.
@@programmingchaos8957 Oh thanks for subscribing to my channel. 🙏 I am glad you found them interestings :)
What software do you use to code?
Processing, which is a free environment for Java (or JavaScript or Python). You can download it from processing.org. Takes almost no time to install and is nice for these quick(ish) programs.
I would LOVE to see a data oriented approach to this, maybe with some kind of ECS implementation.
Dont get me wrong I love me some OOP but this feels like such a classic use case for DOP.
That's a great idea! For now I'm sticking with OOP because it's more straightforward in Java, but if you implement it with ECS let me know how it goes.
@@programmingchaos8957 it might finally be the excuse I need to try out Bevy :) I'll try to share something if I do!
@@htspencer9084 Looking forward to it!
I might use some of these tactics for my simulation
Please let me know how it goes and if you have any different tactics. For finding the nearest particle, with wrapping, I'm pretty sure there's a clever mathematical trick to make the code more compact, but I couldn't find one.
@@programmingchaos8957 would that make it faster because the main issue with my current situation is the speed. Im getting around 5fps and im running it on a fairly good computer
If there were a way to combine both checks for width into one check and both checks for height into one check that could speed things up a bit - there's not a lot of code in the update loop so every instruction matters. I saw a big drop in performance between about 1500 and 3000 particles. Which I think implies that somewhere in that range the array don't fit in cache memory anymore, which slows things down a lot. You might see a big performance increase at some critical particle number.
Another thought, I haven't experimented with it, but my approach of wrapping the particles could be slowing things down a bit. It requires 2 modulus (%) operations everytime, even when the particles don't need to wrap. A simple if might(?) be faster.
Lol so many neat math coding tricks in a vid. Awesome.
Also to deal with the parts movving too fast per frame, you could use a smaller K and calling update many times per frame. In game engines would be called physics substepping.
Thanks! Substepping is a great suggestion. Of course, at other time they move too slow (at least given how impatient I get) so making it adaptive would be ideal.
Really enjoyed this and I spent several hours tweaking my own version. Apparently I can't paste a link in a comment as the comment gets deleted.
Glad you liked it! Let me know if you see any obvious tweaks I should consder. I know there are some changes that would improve efficiency, I was aiming at simply code. I'm not sure what's up with links, I'll double-check the channel settings in case I have something set wrong.
@@programmingchaos8957k
Haven't looked at efficiency, as it seems to run fine on my computer, full screen with 1300 particles. I added the mouse as a type of its own, with a bigger mass - that was fun. I have setParameters() being triggered every 10-20 seconds at random. Each particle has its own random saturation and radius, so this looks nice:
fill(type * colourStep, saturation, 40);
circle(position.x, position.y, radius);
fill(type * colourStep, saturation, 100);
circle(position.x, position.y, radius/2);
Somewhere between 1500 and 3000 particles I see a big drop off. The mouse approach sounds great. In the previous video I showed a model in which the particles only respond to the mouse, which makes it possible to create waves of particle motions. I haven't experimented with saturation or radius - that does sound nice. Good color choices is not my strong suit.
@@programmingchaos8957 The complexity is O(n^2) I guess. Short of a quadtree, the biggest efficiency gain would be avoiding sqrt() calls in mag and normalize. That is, storing squared minDistances and radii values and doing the sum of squared differences of position
Is it in Java of JavaScript?
Java, but Processing has a JavaScript option call P5.js. It's easy to install from the tools(?) menu.
What programming language is this?
It's Java, but using the processing interface, which hides a lot of the Java syntax overhead and includes the easy to use graphical libraries. Processing is free to download from processing.org. I do a lot of more 'serious' programming in C++ and C#, but I really like Processing for smaller projects and testing ideas.
at 33:36 the upper left structure of particles that is purples surrounded by greens and cyan resembles me some eyes
I totally missed that, very cool. Thanks for pointing it out. I've been trying to figure out a way to 'rewind' the results so that when an interesting structure appears its possible to know not only the rules, but also the initial configuration of particles that led to it.
Great video!
Thank you!
Lynn Margulis
It does resemble the organisms she studied. If you're interested in the topic apparently some scientists believe they recently discovered a newly evolved example of a symbiotic organism becoming an organelle: www.newscientist.com/article/2426468-a-bacterium-has-evolved-into-a-new-cellular-structure-inside-algae
@@programmingchaos8957 hey! thanks a lot! congrats on your channel!!!!
@@programmingchaos8957 celebrity gossip: she and carl sagan were early couple
Thank you! Also, if you haven't already you may want to check out the 3D version ua-cam.com/video/G25S5FaMhH8/v-deo.html or the evolving 2D version: ua-cam.com/video/Nor4FxoLT9U/v-deo.html
@@programmingchaos8957 very well done!
very cool 😍
I'm glad you liked it. I'm always looking for interesting projects to share, so if you have anything you'd like to see, let me know.