⬆️ ⬇️

Solar system on LibCanvas





Yesterday there was a great topic, “ The Story of One Habraspor, ” about creating a “galaxy” on HTML5 Canvas, which in itself and its comments inspired me to the response code. I thought, before the end of the documentation, not to write new things on Habra, but, as you can see, it failed) Thanks to kibizoidus for this.



In the topic you will see a description of the process of creating a star system on the latest version of LibCanvas . Fast, optimized, briefly.



I must say that the TZ is not fully implemented. The reason is very simple - having arrived yesterday at 3 am from the Hobbit, I set myself a clear deadline - no more than an hour for everything. Honestly, I considered adding the cursor change in the morning at breakfast, but these are the details) At the hour I did it.

')

Unlike the previous author, I didn’t have to create “Virtualhost in my dev-environment, git-repository” and everything else - all this is always in my battle for libcanvas.github.com . Because I took the standard template and immediately rushed into battle, setting the sky as the background, so as not to look at the lonely blackness:



<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>LibCanvas :: Solar</title> <link href="/files/styles.css" rel="stylesheet" /> <style> html, body { background: url(im/sky.png) } </style> <script src="/files/js/atom.js"></script> <script src="/files/js/libcanvas.js"></script> </head> <body> <p><a href="/">Return to index</a></p> <script> new function () { LibCanvas.extract(); atom.dom(function () { new Solar.Controller(); }); }; </script> <script src="js/controller.js"></script> </body> </html> 




The first steps



First of all, we need to add the application itself. I’ll say right away that the term was very limited, so I didn’t bother with architecture and magic numbers and wrong decisions were scattered all over the code, but it would work as an implementation idea. In addition to creating the application, we immediately add the preloading of two pictures - the image of the Sun and the cutting with images of all the planets . We add pictures to the application's resources so that you can always easily reach them.

 /** @class Solar.Controller */ atom.declare('Solar.Controller', { initialize: function () { this.size = new Size(840, 840); this.app = new App({ size: this.size }); atom.ImagePreloader.run({ planets: 'im/planets.png', sun : 'im/sun.png' }, this.start.bind(this)); }, start: function (images) { // images ready this.app.resources.set( 'images', images ); } }); 




All astronomical objects we will have on a special layer. Their peculiarity is that each of them is updated every frame and there is no sense in solving something special with redrawing. Just set intersection: all so that they all redrawn and invoke: true so that each of them onUpdate method

 /** @class Solar.Controller */ // .. initialize: function () { // .. this.geoLayer = this.app.createLayer({ name: 'geo', invoke: true, intersection: 'all', zIndex: 2 }); // .. 




The simplest thing we can do is add a drawing of the sun. It is static and corny in the center of the frame. Create a class for it that will draw the necessary image to a point, inherit from App.Element so that you can add to the application:

 /** @class Solar.Sun */ atom.declare('Solar.Sun', App.Element, { renderTo: function (ctx, resources) { ctx.drawImage({ image : resources.get('images').get('sun'), center: this.shape.center }); } }); 




After that in our controller we create our Sun:

 /** @class Solar.Controller */ // .. start: function (images) { // .. this.sun = new Solar.Sun(this.geoLayer, { shape: new Circle(this.app.rectangle.center, 50) }); // .. 




Now we have a lonely Sun in the system:





Planets



The next target is the planets. We have 12 planets. The first will be at a distance of 90 pixels from the center of the system, each next 26 pixels further. The first passes a circle in 40 seconds, each following for 20 seconds longer. And every planet has a name



 /** @class Solar.Controller */ atom.declare('Solar.Controller', { names : 'Selene Mimas Ares Enceladus Tethys Dione Zeus Rhea Titan Janus Hyperion Iapetus' .split(' '), // .. start: function (images) { // .. for (var i = 12; i--;) { var planet = new Solar.Planet(this.geoLayer, { sun : this.sun, radius: 90 + i * 26, time : 40 + i * 20, image : i, zIndex: 0, name : this.names[i] }); } // .. 




We take the planet into orbit very easily - first we place it in the center of the system, and then we shift it to the right by the radius of the orbit.



  this.center = this.solarCenter.clone(); this.center.move([ this.radius, 0 ]); 




The rotation speed is not more difficult to find - we need to fly 360 degrees in 'time' in seconds or 360/1000 in time in milliseconds:



  this.rotatePerMs = (360).degree() / 1000 / this.settings.get('time'); 




The necessary picture from sprites is cut out very simply - we shift to the right by the planet index:



  getImagePart: function () { var x = this.settings.get('image'); return this.layer.app.resources.get('images') .get('planets') .sprite(new Rectangle([ x*this.size.width,0 ],this.size)); }, 




The process of rotation of the planet itself - simply rotate the center of the planet around the center of the system using the LibCanvas.Point.rotate method. normalizeAngle needed so that the angle is always between 0 and 360 degrees.



  rotate: function (angle) { if (angle == null) angle = Number.random(0, 360).degree(); this.angle = (this.angle + angle).normalizeAngle(); this.center.rotate(angle, this.solarCenter); return this; }, 




onUpdate add interactivity - every call to the onUpdate method onUpdate planet, not forgetting to make an amendment for the time that has passed since the previous call. onUpdate , like onUpdate , are renderTo built into the LibCanvas framework that can be overridden and changed behavior.

  onUpdate: function (time) { this.rotate(time * this.rotatePerMs); this.redraw(); }, 




And most importantly - add image rendering:



  renderTo: function (ctx) { ctx.drawImage({ image : this.image, center: this.center, angle : this.angle }); } 




Hidden text
 /** @class Solar.Planet */ atom.declare('Solar.Planet', App.Element, { angle: 0, configure: function () { this.size = new Size(26, 26); this.center = this.solarCenter.clone(); this.center.move([ this.radius, 0 ]); this.rotatePerMs = (360).degree() / 1000 / this.settings.get('time'); this.shape = new Circle(this.center, this.size.width/2); this.image = this.getImagePart(); this.rotate(); }, getImagePart: function () { var x = this.settings.get('image'); return this.layer.app.resources.get('images') .get('planets') .sprite(new Rectangle([x*this.size.width,0],this.size)); }, get radius () { return this.settings.get('radius'); }, get solarCenter () { return this.settings.get('sun').shape.center; }, rotate: function (angle) { if (angle == null) angle = Number.random(0, 360).degree(); this.angle = (this.angle + angle).normalizeAngle(); this.center.rotate(angle, this.solarCenter); return this; }, onUpdate: function (time) { this.rotate(time * this.rotatePerMs); this.redraw(); }, renderTo: function (ctx) { ctx.drawImage({ image : this.image, center: this.center, angle : this.angle }); } }); 






Now we have planets in the system:





Orbits



The next task is to add orbits. They have significant differences from the planets, because we will use a different approach and a separate layer.

First, they are static. Unlike planets, they do not change, or they change very rarely. In addition to this - they are much larger than the planets and their rendering is resource-intensive, therefore we will produce it as little as possible.



Create a layer. We don’t need an onUpdate call here, and we will manage the intersections manually. All the same, these intersections of objects with each other, as we see, especially and no. In addition, in order not to return, immediately add a method to create an orbit on the planet

 /** @class Solar.Controller */ // .. initialize: function () { // .. this.orbitLayer = this.app.createLayer({ name: 'orbit', intersection: 'manual', zIndex: 1 }); // .. start: function (images) { // .. for (var i = 12; i--;) { var planet = new Solar.Planet(this.geoLayer, { // .. }); planet.createOrbit(this.orbitLayer, i); // <=== // .. } // .. 




 /** @class Solar.Planet */ // .. createOrbit: function (layer, z) { return this.orbit = new Solar.Orbit(layer, { planet: this, zIndex: z }); }, // .. 




The original orbit code is fairly simple. Create a Circle , which will be the basis of our drawing, in the renderTo method renderTo simply stroke this circle. The only thing that has now been added is the clearPrevious method, which changes the principle of cleaning the layer from this object - we do not roughly clear the contents of the BoundingRectangle, but carefully draw the circle stroke using the inverted ctx.clear(this.shape, true) :

 /** @class Solar.Orbit */ atom.declare('Solar.Orbit', App.Element, { configure: function () { this.shape = new Circle(this.planet.solarCenter, this.planet.radius); }, get planet () { return this.settings.get('planet'); }, clearPrevious: function (ctx) { ctx.clear(this.shape, true); }, renderTo: function (ctx, resources) { ctx.stroke(this.shape, 'rgba(0,192,255,0.5)'); } }); 




Now our application has acquired a view close to the final one and it remains to add user interaction.





Subscribe to the mouse



Create a LibCanvas.Mouse object that catches mouse events of a specific dom element and a LibCanvas.App.MouseHandler object that will process these events and redirect to the corresponding application element.

 /** @class Solar.Controller */ // .. start: function (images) { var mouse, mouseHandler; mouse = new Mouse(this.app.container.bounds); mouseHandler = new App.MouseHandler({ mouse: mouse, app: this.app }); this.app.resources.set({ images: images, mouse : mouse, mouseHandler: mouseHandler }); // .. for (var i = 12; i--;) { var planet = new Solar.Planet(this.geoLayer, { // .. }); planet.createOrbit(this.orbitLayer, i); mouseHandler.subscribe( planet ); mouseHandler.subscribe( planet.orbit ); } } // .. 




The mouse is already caught, but so far we can not notice it. Let's start with the orbits. Now she is caught simply if she is inside her. THOSE. Any place between the sun and the planet is considered a trigger for the orbit to trigger. We will change this by overriding the isTriggerPoint method. Now mouse events will fire into orbit only within 13 pixels from it.

 /** @class Solar.Orbit */ // .. isTriggerPoint: function (point) { var distance = this.planet.solarCenter.distanceTo(point); return (this.planet.radius - distance).abs() < 13; }, // .. 




Immediately, to check how it works, we activate the hover event using the Clickable behavior and slightly modify the rendering method:



 /** @class Solar.Orbit */ // .. configure: function () { // .. new App.Clickable( this, this.redraw ).start(); }, // .. renderTo: function (ctx, resources) { if (this.hover) { ctx.stroke(this.shape, 'rgba(255,64,64,0.8)'); } else { ctx.stroke(this.shape, 'rgba(0,192,255,0.5)'); } } // .. 








Now we see that the orbits change when you hover the mouse. But when you hover on a planet, the mouse is blocked and no longer reaches the orbit. Slightly change this behavior by adding a hover to the planet and checking both of these states:



 /** @class Solar.Orbit */ // .. isHover: function () { return this.hover || this.planet.hover; }, renderTo: function (ctx, resources) { if (this.isHover()) { // .. } // .. 




And since when hovering over the planet, we still need to redraw the orbit every frame (the circle around the planet is shifted) - add redraw to onUpdate

 /** @class Solar.Planet */ // .. configure: function () { // .. new App.Clickable( this, this.redraw ).start(); }, // .. onUpdate: function (time) { // .. if (this.orbit.isHover()) this.orbit.redraw(); }, // .. 




Now the planet does not block the hover and we can do orbit styling. We will perform the task very easily. First draw the orbit. Then erase the shape of the planet, and then trace the shape of the planet. Thus it will turn out quickly and relaxed to achieve the result:



 /** @class Solar.Orbit */ // .. renderTo: function (ctx, resources) { if (this.isHover()) { ctx.save(); ctx.set({ strokeStyle: 'rgb(0,192,255)', lineWidth: 3 }); ctx.stroke(this.shape); ctx.clear(this.planet.shape); ctx.stroke(this.planet.shape); ctx.restore(); } else { ctx.stroke(this.shape, 'rgba(0,192,255,0.5)'); } } 




But here we will be in trouble - our "cleaner" knows nothing about the thickness of the orbit, especially, that it still has a "hernia" from the planet. We redefine the next LibCanvas method, saveCurrentBoundingShape , which will save exactly where the hernia was last, if any, at all. It will also be called when necessary automatically:

 /** @class Solar.Orbit */ // .. saveCurrentBoundingShape: function () { if (this.isHover()) { this.previousBoundingShape = this.planet.shape.clone().grow(6); } else { this.previousBoundingShape = null; } return this; }, 




Now we can inform our cleaner about new conditions. If there was no hernia, we clean it old. Otherwise, we increase the thickness in order to erase well, clean the hernia and then restore the canvas settings:

 /** @class Solar.Orbit */ // .. clearPrevious: function (ctx) { if (this.previousBoundingShape) { ctx.save(); ctx.set({ lineWidth: 4 }); ctx.clear(this.previousBoundingShape); ctx.clear(this.shape, true); ctx.restore(); } else { ctx.clear(this.shape, true); } }, 




The result is exactly expected:





Information about the planet



The last thing to add is a pop-up window with the name of the planet. We could subscribe to the mouse over and mouseout on Solar.Planet , but they only work when the mouse is moving, i.e. if the planet leaves the stationary mouse, the mouseout will not work. Therefore, we take the point of the mouse and every frame we check if it is located above our planet.

 /** @class Solar.Planet */ // .. configure: function () { // .. this.mousePoint = this.layer.app.resources.get('mouse').point; this.info = new Solar.Info(this.layer, { planet: this, zIndex: 1 }); }, checkStatus: function (visible) { if (this.info.isVisible() != visible) { this.info[visible ? 'show' : 'hide'](); } }, onUpdate: function (time) { // .. this.checkStatus(this.isTriggerPoint(this.mousePoint)); //        if (this.info.isVisible()) this.info.updateShape(this.shape.center); }, // .. 




settings: { hidden: true } is one of the LibCanvas settings. This element will not participate in drawing in any way, but it still catches mouse events if it is signed. Therefore, we create a simple and logical class Solar.Info using this feature.



 /** @class Solar.Info */ atom.declare('Solar.Info', App.Element, { settings: { hidden: true }, get planet () { return this.settings.get('planet'); }, configure: function () { this.shape = new Rectangle(0,0,100,30); }, updateShape: function (from) { this.shape.moveTo(from).move([20,10]) }, show: function () { this.settings.set({ hidden: false }); this.redraw(); }, hide: function () { this.settings.set({ hidden: true }); this.redraw(); }, renderTo: function (ctx) { ctx.fill(this.shape, '#002244'); ctx.text({ to : this.shape, text : this.planet.settings.get('name'), color: '#0ff', align: 'center', optimize: false, padding: 3 }) } }); 




And we get a predictable result:



The only thing that remains is to add a change in the mouse cursor when hovering over the planet:

 /** @class Solar.Planet */ // .. checkStatus: function (visible) { if (this.info.isVisible() != visible) { this.info[visible ? 'show' : 'hide'](); this.layer.dom.element.css('cursor', visible ? 'pointer' : 'default'); } }, // .. 




Result



We watch result on GitHub , and also source codes .



Also comparable to the solution from the previous topic:

1. Significantly less code

2. Significantly higher performance



Check in profile. The higher the percentage (program) - the better. On an empty application, it reaches 100%.



Original version:





Version on LibCanvas:





Blank tab:





I also checked the actual performance on a weak laptop (FPS / load of one core of the kernel):



Original version - download up to 100%, 44 fps:





LibCanvas version - download up to 40%, stable 60 fps:





Write code in pleasure, use good tools, enjoy. If you have questions, but you do not have the opportunity to ask on Habré - write to email shocksilien@gmail.com

Source: https://habr.com/ru/post/163893/



All Articles