📜 ⬆️ ⬇️

The story of one habraspor



Not so long ago, looking deep into an interesting enough note about a game based on JS / Canvas (with my mistakes and misconceptions that I had too, which is no secret, I had a good feeling), I came across another batch of frankly holivarny comments , after reading the world outside was gray and bleak, the food had lost its taste, and the favorite tea turned out to be unsweetened. And at that moment either the stars came together, or urgent and important bugs and features were a little less yesterday, but I decided to get involved in an argument with rather sharp theses and stand up for my favorite technology, which was so frankly watered than it was. It would have remained all this at the level of pointless transfer of packages with arguments over the fence, if I had not decided to write RussianSpy to the same branch, and not about abstract parrots, which are easier to rewrite in 3D, but about a very specific task. And the flashed phrase "I can send TZ ..." smoothly hinted that the evening promises to be interesting.


Training

12/21/12, 2:29

Of course, at this point, you had to stop, dig up beautiful demos from the network, throw a couple of ounces on the fan and calm down. But we are not looking for easy ways ...
')
Having read the last comment , which has already clearly touched my ego, without hesitation, I reply “send TK”, and literally in 5-10 minutes I receive a letter in Habr's mailbox with a contact request. I answer, request for contacts in Skype, greeting, let's go. Got an archive with graphics. TK was simple enough, on the one hand, but at the same time - interesting as a practice. Yes, and my own pride (mean such a lady, yeah) now I just would not let my face hit in the dirt.

If you describe the problem briefly - it was necessary to build a generator of heliocentric planetary systems with a small GUI tied to the planets and their orbits. In 2D. It was described, by and large, briefly in the dialogue itself, so that several more clarifying questions arose. But in general, everything was quite transparent. At 2:53, I was already looking at what I had and was thinking about how to bring all this to life.

Terms of Reference
The planets revolve around the star in a clockwise direction. The rotation speed of the nearest planet is 40 seconds per revolution. The time for the rotation of each subsequent planet is 20 seconds longer than the previous one. The composition of the system is random. With each update, the planets occupy a random place in their orbit, from which they start the rotation and the picture for the planet is also random.

When you hover the mouse on a planet, it is highlighted with a round frame, the image of the orbit also changes as indicated in the layout.

When you click on the planet, the menu appears. When you hover the mouse on the planet and when you click on it, the animation of this planet stops, the rest of the planets continue to move.

When you hover the mouse into orbit - the orbit is highlighted, the planet does not.

When you hover over a planet or orbit, the mouse cursor changes to a pointer.

Should work: Opera 12+, IE9 +, current versions of Chrome, FF and Safari under the stub.

12/21/12 6:13 AM

In general, the implementation of the task by this time has become more or less clear and clear. Practically, the first thing I did was to figure out what steps I needed to do to successfully draw at least the central star, and I stopped there. Created a separate Virtualhost in his dev-environment, git-repository, zakomitil index.html and went to bed. On the way, thoughts about animation, transformations with turns into offscreen-canvas, problems of calculating the mouse coordinates and a ton of all sweets were swarming in my head. Because of this, I could not fall asleep long enough, but nature took its toll.

Process

12/22/12 9:00 PM

Having dedicated Friday to all unfinished business to my principled demo, I returned on Saturday evening, after the family went to sleep and I had the freedom to think, to smoke a lot and to drink coffee cans.

From the experience of working on FiveGUI I realized that something like this would be needed in this project. I decided to implement everything through a kind of AnimationController, in which there will be a link to the main drawing panel, and dynamically collect the picture in offscreen-canvass of his child-objects. Having a little rummaged in the Mozilla Developers Network and on HTML5Rocks, having restored the knowledge around requestAnimationFrame, I began to work on the basic structure of the project and the objects that I will eventually draw.

First of all, I broke the entire project at the root of Vitrualhost into js / css / img, put all the inherited graphics into the appropriate folder and sketched an example of the structure with links to the images. The problem of preloading these very images arose to its full height, and the main controller got a loader of graphics. I implemented it, in principle, quite clumsy - simply comparing the total number of links in the structure with the counter of loaded images, which was updated through the closure in the load method of each image.

The overall concept at the time
//main.js var PlanetController = new Planets({}); //   var resources = { //, stars: { "1": "/img/stars/1.png" } //      ^_^ } for(var a in resources) {//     for(var i in resources[a]) { PlanetController.pushResource(a+"-"+i, resources[a][i]); } } //Planets.js var Planets = function(){ /* ... */ } // ""  Planets.prototype.loadResources = function(cb) { //   var self = this; self.data.loadedResourcesCount = 0; self.data.resourcesCount = Helpers.objLength(self.resources); for(var resource in self.resources) { if(self.resources.hasOwnProperty(resource)) { //  ,      , .   ! ! var tempImg = new Image(); tempImg.onload = function() { self.data.loadedResourcesCount++; if(self.data.loadedResourcesCount == self.data.resourcesCount) { self.startTimestamp = new Date(); self._flags['_resources_loaded'] = true; cb(); } } tempImg.src = self.resources[resource]; self.resources[resource] = tempImg; } } } 



Then it was necessary somehow to start the project itself for drawing after successfully loading the resources. I did not bother with the event model (which, in consequence, when finalizing, can play a cruel joke with me) and decided to do it through a callback call if the counters coincide, in fact - after loading the last picture. It's about time I thought. The callback itself and all its surroundings were delivered to a separate main.js file, the callback decided to call the window in the visibility area in order to have direct access to its controller without the hemorrhoid (by that time it was already in the window).

I tweaked a few typos and obvious bugs, got a call to callback and alert in the console that all images were successfully loaded and moved further into the jungle of objects.

Here is the time of the most interesting. I already had a Helpers.js file, which was intended for service functions and everything that was extended, which I intended to use in the project. Further it was necessary only to think over the structure of objects.



And she, as seen in the image above, was born fairly quickly. Each object was inherited from the main parent (in my case, the Element object) with the help of a simple combination through the replacement of the prototype (the famous Extend function). It seemed to me - my decision was the most logical, based on the structure of any "solar" system. We have a primary parent - the Star, which includes a number of Orbits. Orbit has a baby Planet, which already includes pop-up windows with information about itself (Popup - Float) and some actions (Popup - Static - Options). If you look at the scheme - I have a fairly global error that can become a "feature" - several planets in the same orbit. I, frankly, did not go into details, is it possible, but, I think, in this way it will be possible to quickly build a relationship between the planets in one orbit and in the future make the satellite subspecies of the planet. But it is in the future.

At the current moment, the basic structure was successfully logged out and the next step was the drawing. Although it would be the central star.

Then I remembered my optimization of porting to JS TinyTower and how I was suffering there with rendering speed “hot”, therefore, ahead of time, I made in each object a drawing to the internal offscreen-canvas for this object personally, with subsequent assembly from the bottom up. The decision to replace canvas for the current object with the one that came back from its cub, if the cub is bigger in size than the current canvas of the parent, immediately arose. Thus at the end of the chain there will always be the largest panel with already drawn data for all the planets, stars and orbits. It turned out such a multi-faceted analogue of Z-buffer for video cards. And as my experience shows, drawing complex shapes, graphics and other things into memory is much faster.

Drawing Stars did not take much time. Through the size of the image of the Star and the general canvas, a calculated the offset and hung the picture in its place.

With the Orbit it was a bit more complicated. Since each orbit was given a radius, it was necessary to prepare a personal canvas for it and draw an arc with a diameter of Pi * 2 radians. Recalling his pain in converting radians to degrees and back, he decided that it was still better to rebuild brains than to make a garden with an envelope.

With the Planets was the most interesting. Not only did the Planet need to be positioned in Orbit, which was realized a little higher in the Orbit object, it also had to be put in reserve for future rotation when calling the draw.

In general, drawing everything looked quite banal: Kids were added to the parent object, then the parent ran through all the children, each sequentially called .draw (), and combined all the results to his canvas, then returned this canvas to your own parent. Thus, .draw (), called from the controller, received the latest actual version of what was supposed to be drawn, cleaned the main panel and in one step drawed a new frame on the screen.

All this is neatly wrapped in a cross-browser requestAnimationFrame ().

In fact, it looks like this
 //this.dc - offscreen-canvas    //this.dctx - offscreen-canvas    //Planets.js Planets.prototype.draw = function(timestamp) { var self = this; self.frame++; self.cx.clearRect(0, 0, this.width, this.height); //  for(var a in self.elements) { if(self.elements.hasOwnProperty(a)) { //      var drawData = this.elements[a].draw(timestamp); self.cx.save(); self.cx.translate((this.width-drawData.width)/2, (this.height-drawData.height)/2); //    self.cx.drawImage(drawData, self.elements[a].getX(), self.elements[a].getY()); self.cx.restore(); } } requestAnimFrame(function(){self.draw(new Date());}); //   loop    } // Star.js Star.prototype.draw = function(timestamp) { var orbitData = null; var drawData = null; this.dctx.clearRect(0, 0, this.width, this.height); // Compose all orbits for(var a in this.orbits) { if(this.orbits.hasOwnProperty(a)) { drawData = this.orbits[a].draw(timestamp); //   canvas'   ,   if(orbitData == null) { orbitData = drawData; continue; } if(orbitData.width > drawData.width) { orbitData.getContext("2d").drawImage( drawData, (orbitData.width - drawData.width)/2, (orbitData.height - drawData.height)/2 ); continue; } drawData.getContext("2d").drawImage( orbitData, (drawData.width - orbitData.width)/2, (drawData.height - orbitData.height)/2 ); orbitData = drawData; } } // Draw Star if(orbitData.width > this.width) { this.setWidth(orbitData.width); this.setHeight(orbitData.height); } this.dctx.drawImage(orbitData, 0, 0); this.dctx.drawImage(this.image, (this.dc.width - this.image.width) / 2, (this.dc.width - this.image.width) / 2 ); return this.dc; } //Orbit.js Orbit.prototype.draw = function(timestamp) { var drawData = null; this.dctx.clearRect(0, 0, this.width, this.height); this.dctx.strokeStyle = "rgba(85, 183, 242, .5)"; this.dctx.lineWidth = 1; this.dctx.beginPath(); this.dctx.arc(this.width/2, this.height/2, this.radius, 0, Math.PI*2, true); this.dctx.closePath(); this.dctx.stroke(); for(var a in this.planets) { if(this.planets.hasOwnProperty(a)) { drawData = this.planets[a].draw(timestamp); this.dctx.save(); this.dctx.translate(this.width/2, this.height/2); this.dctx.rotate(this.planets[a].getPosition()); this.dctx.drawImage(drawData, (this.planets[a].width)/2*-1, (this.height/2-this.planets[a].height) ); this.dctx.restore(); } } this.update(); return this.dc; } //Planet.js Planet.prototype.draw = function(timestamp) { this.dctx.clearRect(0,0,this.dc.width, this.dc.height); this.dctx.drawImage(this.image, 0, 0); return this.dc; } 


Then came the time of animation and events ...

12/23/12 2:00 AM

I was lucky to animate in my example I needed only planets, or rather their movement. Having once again complained to unstable fps, I started the start timestamp in the base controller and started transmitting the current timestamp starting from the main parent with each update to draw (). This way allows you to do animation for all objects at the same time offset, not paying attention to the duration of the operations. In the Technical Specification, the speed of the planet was indicated by the angular value, therefore, my speed of the planets also began to be expressed as Math.PI * 2 /. Having added the Planet object with the method of calculating the current angular position in Orbit, added the translate-rotate chain to render the planet, while taking the angular value of the rotation directly from the Planet object.

Thus, the animation itself was implemented fairly quickly and without any problems. When my Planets "flew" - it was a small victory. Almost immediately, I replaced the dull white background with a “black eclipse” from the image sources, and my planetary system began to look like something interesting. I was pleased with myself and boldly moved on.

I borrowed a stack of events with little or no innovation from myself, slightly adjusting it for my own needs. In general approximation, any mouseevent on the body object was transferred to the controller, and the controller already passed it to the children, successively checking whether a blockage occurred.

The first event that I implemented is the “highlighting” of the orbit during hover. Having added Helpers.js with the mathematics of the distance between two points, I began to check whether the current distance from the center of the star to the Mouse-pointer is the same, and if it coincides with a certain error, I increased the thickness of the orbit line and changed its color.

With the Planets, as usual, everything was much more interesting. On the one hand, the task was facilitated by the fact that event handling could be done only when the parent, Orbit, processed its hover event, on the other hand, the Planet is still round. The isPointInPath () method came to the aid of the good old memory. A simple test of the state of the parent of the Planet, I began to determine whether I need to handle an event on the Planet, draw a test transparent line in the form of an arc with a long Math.PI * 2 (circle, aha), wrap it in a beginPath () - endPath () block, broadcast The current coordinates of the mouse and in the parent checked whether the mouse pointer is in the right place.

At about the same stage, pop-up pop-ups with service information were added. I got on the Internet spaces lists of planets, nicknames and alliances. (Planets are in fact the names of the celestial bodies of the solar system in addition to real planets, Nicky is the real nicknames of NBA players, but alliances have already been taken from the generator), added them to the planetary generation cycle and almost immediately painted the first popup with hover. It turned out cool, and I was confused with a static pop-up from TK.

Actually, the difference between static and dynamic popups is not only that they look different, but also that static popu blocks event processing for the rest of the panel. I had to finish a small crutch into the main event handling method with a popup check. Well, in addition to the flaws that already existed - the link to the popup should always be present in the controller, since it exists above everything in principle (drawn even over the star), but physically belongs to the planet. Here is such a discord with which I tormented for quite a long time.

Adding “options” to the static popup, handling mouse events when clicking on the Planet and the option was performed in almost the same style as hover for the rest of the objects, except that event handlers were added in the form of callbacks in each option.

Finally, it was possible to do fixes and bugs, debug and show. Almost everything was completed and, surprisingly, after launching in the main browsers on my stub there were no special problems. So at 7:04 I, satisfied with myself, went to bed.

Conclusion

12/23/12 9:16 PM

Having debugged the last skolzskie moments, clearly knocking out of the TZ, adding a pointer and combing all that was left, threw everything on your hosting, asked friends to look at Donkey, after confirming that everything works, clicks and rotates - sent the link RussianSpy . After about 10 minutes, the first reaction arrived, cool enough, as it seemed to me, but then I was wrong, and my opponent rather checked how all this was done and why it does not slow down and does not fall. After 15 minutes of discussing technical issues, the soul felt warm and good, the old paints returned to the world and the birds of paradise sang outside the window. Alexander saw that the “non-braking and working” Canvas is possible and, moreover, right before his eyes. Of course there were comments about the "academic" of this example, there was a bug with the events in the opera, but in general, I was able to get my way and show how well it works.
It is always nice to hear this in your address.
[23.12.12, 21:40:28] RussianSpy: In general, you impressed me.
...
[23.12.12, 21:48:57] RussianSpy: You know - nevertheless, you managed to demonstrate a non-braking solution to this problem.
[23.12.12, 21:49:18] RussianSpy: So you dug deep enough into the topic of canvas.

Still, I thought at this moment. I can find a common language with canvas.


Having discussed some points for this article, we spent about an hour discussing what I showed and, in general, what I had about Canvas. Shown by FiveGUI, the slaughtered TinyTower port on JS, which Sasha was very interested in and we agreed not to lose sight of each other.

After that, you could go to rest contentedly.

findings


What conclusions did I draw from this? A lot of everything.

And finally ...


Sources on Github
Worker Demo

Thanks for attention!

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


All Articles