
This topic will be different from the previous topic
Classic sapper on html5 and LibCanvas . It may even be called a continuation. And the first part contained step-by-step and detailed instructions on how to make a toy work, then in this part there will be a couple of interesting techniques, how to “render” it.
')
If you are new to this business, you should start with the first part. For those who want to go deep, I will consider the following topics on the example of an isometric sapper built on the basis of LibCanvas:
- Isometric view
- Optimizing rendering speed through dirty hack
- Sprite animations
- Draggable layers
- Mouse handler optimization according to application features
Lyrical digression
I still propose not to go into the senseless criticism a la “classical sapper is better”, “gates of this form are nonsense” and “you have a little wrong formula there”. The goal was to make a cute toy in a short time (about 8 hours for the full implementation of the art part and about 4 hours for the full implementation of the code at a leisurely pace in your free time). And reveal on the basis of this toy some approaches.
Isometric view
Let's start with the easiest and most interesting. Isometric games on LibCanvas are implemented by combining two tools - the
LibCanvas.App
framework, which was described in the previous two topics and the
IsometricEngine.Projection
class.
The peculiarity of
IsometricEngine.Projection
is that it does not implement anything by itself. It only provides a convenient and fast way to convert 3d coordinates in space to 2d isometric coordinates and back.
var projection = new IsometricEngine.Projection({ factor: new Point3D(1, 0.5, 1) });
When translating from 2d coordinates, it is always unclear in which plane to count the point, therefore, in addition, you have to specify the future z-coordinateBy the way, what is this
factor
? In theory, the correct isometric projection is a projection with an angle of 120 °, but in practice, in toys, a projection with an angle of ~ 117 degrees is used so that the lines fall into the pixel grid.

That's just the factor and allows you to set the aspect ratio. You can choose any of the approaches:
I didn’t give a damn about all the rules in my application and just made the factor equal to the size of the picture, so I have the left corner of the picture in the coordinates
[0;0;0]
, and the right one in
[1;1;0]
factor: new Point3D(90, 52, 54)

So how did I adapt this tool to the requirements of the application? A very simple. Create a cell element like this, where the cell is simply drawn around the center of the shape.
atom.declare( 'IsoMines.Cell', App.Element, { renderTo: function (ctx, resources) { ctx.drawImage({ image : resources.get('images').get('static-carcass'), center: this.shape.center }); } });
And then, using the projection, I created the necessary number of cells, passing them polygons:
createPolygon: function (point) { var p = this.projection; return new Polygon( p.toIsometric(new Point3D(point.x , point.y , 0)), p.toIsometric(new Point3D(point.x+1, point.y , 0)), p.toIsometric(new Point3D(point.x+1, point.y+1, 0)), p.toIsometric(new Point3D(point.x , point.y+1, 0)) ); }, createCells: function () { var size = this.fieldSize, x, y, point; for (x = size.width; x--;) for (y = size.height; y--;) { point = new Point(x, y); new IsoMines.Cell(this.layer, { point : point, shape : this.createPoly(point) }); }
I highly recommend reading the
documentation - it's quite interesting there, IMHO.
Rendering speed optimization
And now we will look at our application and pay attention to its feature. The beauty is that our objects do not move relative to each other and never intersect, they just touch. Moreover, the objects themselves are completely opaque. Therefore, when changing, you can act very brazenly - do not erase anything, but simply draw a new cell image on top of the old one.
To do this, firstly, you need to disable the intersection check in
LibCanvas.App
when creating a layer
this.layer = this.app.createLayer({ intersection: 'manual' });
Second, override the cleanup method by making it empty:
atom.declare( 'IsoMines.Cell', App.Element, { clearPrevious: function () {} });
Suppose we cannot completely abandon cleaning for some reason. For example ... Our image has translucent areas or something like that. As a result, you
will encounter a problem like a
kazmiruk user:

This comes from the two “defaults” of
LibCanvas.App
- the boundingRectangle is considered the bounding figure (boundingShape), and it erases it precisely by it. It is enough to override any of these behaviors (only one of them, both are meaningless):
atom.declare( 'IsoMines.Cell', App.Element, {
In fact, this optimization can be used for all static or semi-static layers. Do not forget that we can have several different Layers and each of them has its own behavior strategy:
this.layerStatic = this.app.createLayer({ intersection: 'manual' }); this.layerDynamic = this.app.createLayer({ intersection: 'auto' });
Sprite animations
The next interesting topic is how to insert and make animation work — beautiful pictures a la gifs. For these purposes, there is one of my favorite plugins -
Animation .
The basic idea is that we transfer a png-file, which contains a lot of animation frames, which we then assemble into “videos”. It looks something like this, but with translucency instead of squares in the background, twice as many frames and the size of each frame:

.
Creating an animation is divided into three stages:
1. Cutting frames
Using
Animation.Frames we cut our grid image into many small pictures:
var frames = new Animation.Frames( image, 180, 104 );
You can have many different animations in one sprite. Cutting should be done only once, and then use it for all prototypes.
2. Prototype animation
Using
Animation.Sheet we create a general description of the animation - frame order, delay, obsession and give a link to the frames that were cut above. Each animation prototype should occur only once per application. For example, if you have an explosion animation that occurs many times per application, you only need to create its
Animation.Sheet
once.
In the isometric sapper, I needed three animations - opening and closing the lock, opening and closing the doors. They had the same settings, frames, only the name and the frame order differed, therefore I did it all short and beautifully:
this.animationSheets = atom.object.map({ opening : atom.array.range( 12, 23), closing : atom.array.range( 23, 12), locking : atom.array.range( 0, 11), unlocking: atom.array.range( 11, 0) }, function (sequence) { return new Animation.Sheet({ frames: frames, delay : 40, sequence: sequence }); }); console.log( this.animationSheets );

3. The essence of animation
But for the immediate launch, every time at the start, an
Animation object is created, where we transfer the callbacks and we can hang on to the events. In the sapper, I organized the launch of animations by switching the state:
Animated state switching in IsoMines.Cell atom.declare( 'IsoMines.Cell', App.Element, { preStates: { opened: 'opening', closed: 'unlocking', locked: 'locking' }, changeState: function (state, callback) { this.state = this.preStates[state]; this.animation = new Animation({ sheet : this.sheets[this.state], onUpdate: this.redraw, onStop : function () { this.state = state; this.animation = null; this.redraw(); callback && callback.call(this); }.bind(this) }); }, getGatesImage: function () { return (this.animation && this.animation.get()) || 'gates-' + this.state; }, renderTo: function (ctx) { this.drawImage(ctx, this.getGatesImage()); this.drawImage(ctx, 'static-carcass'); }, drawImage: function (ctx, image) { if (typeof image == 'string') { image = this.layer.app.resources.get('images').get(image); } ctx.drawImage({ image : image, center: this.shape.center }); }, });
Draggable layers
Our application turned out quite large and even with a full-screen does not fit into the computer screen. It is important for us that the user can reach any cell. Therefore we will fasten the “draggable” layer. T.K. The left and right mouse buttons are busy with us - we will fasten the scroll by pressing and dragging the mouse wheel, and especially for the operatives we will have to make an alternative option through shift + click. Not very convenient, but quite suitable for the demo. I think that in full-screen mode it would be ideal to scroll if the mouse is near the borders, but this was beyond the scope of the article.
The principle is very simple, although it is one of the few classes not covered in the documentation. Hope to fix it soon).
So, first we will use App.LayerShift, which allows you to move a layer along with all its elements and set frames for shifting (if we don’t want to be dragged off to infinity somewhere, and then we could not find it).
After that we will use the built-in class
App.Dragger
, which allows dragging our layer. In the start-up callback, we define under what conditions this drag should be started.
Well and, depending on the mode, we will define the boundaries of the dredge - in the full screen and minimized state they will be different.
Make the layer shake initDragger: function () { this.shift = new App.LayerShift(this.layer); this.updateShiftLimit(); new App.Dragger( this.mouse ) .addLayerShift( this.shift ) .start(function (e) { return e.button == 1 || e.shiftKey; }); }, updateShiftLimit: function () { var padding = new Point(64, 64); this.shift.setLimitShift(new Rectangle( new Point(this.app.container.size) .move(this.layerSize, true) .move(padding, true), padding )); },
It is worth noting that by default the drawing is frozen during a drag. I spotted this optimization in old toys, such as the Pharaon and Caesar 3 - when I shifted the map there - the animations stopped, and you could notice this clearly by “leaning into the wall”. This behavior is quite easy to change, but it will require its own heir
App.Dragger
Mouse handler optimization
A topic that is affected at the very last moment of developing an application is optimization with a profiler.
The fact is that the default handler initially works according to a rather slow algorithm, but a very universal algorithm - it checks all the shapes of the elements to see if the mouse belongs, and if it doesn’t feel at all on the sizes 5 * 5, then the professional size 30 * 16 is half a thousand elements that need to go through each mouse movement, and at a size of 100 * 100 will be
unrealistic 10,000 objects . The square growth on the face (
But each application has its own optimization methods - fast algorithms and caching. To do this, MouseHandler has the opportunity to transfer its own “search engine of elements”, to the constructor of which we can transfer all the necessary data:
this.mouseHandler = new App.MouseHandler({ mouse: this.mouse, app: this.app, search: new IsoMines.FastSearch(this.shift, this.projection) });
In the case of our game, we will put the entire element in an indexed hash, and then search through the method
IsometricEngine.Projection.to3D
- so instead of the complexity
O(N
2 )
we get the complexity
O(C)
- the constant search speed of the element.
Quick search for the cell that was clicked atom.declare( 'IsoMines.FastSearch', App.ElementsMouseSearch, { initialize: function (shift, projection) { this.projection = projection; this.shift = shift; this.cells = {}; }, add: function (cell) { return this.set(cell, cell); }, remove: function (cell) { return this.set(cell, null); }, set: function (cell, value) { this.cells[cell.point.y + '.' + cell.point.x] = value; return this; }, findByPoint: function (point) { point = point.clone().move(this.shift.getShift(), true); var path = this.projection.to3D(point), cell = this.cells[Math.floor(path.y) + '.' + Math.floor(path.x)]; return cell ? [ cell ] : []; } });
To check the quality of the optimization, I made a map of about 1000 elements (33x33), first turned off the quick search, drove the mouse over the field under the profiler for a long time, and then turned on the quick search and drove the mouse over the field again. Result:
Before:

After:

Application load dropped from 74.4% to 3.4% - more than twenty times in such a simple way. In my opinion, the particular advantage of this method is that it allows you to quickly prototype an application using the default algorithm and transfer the optimization to a later date.
P.S
In a couple of weeks I will be speaking on the
JavaScript frameworks day in Kiev with the theme “AtomJS and LibCanvas”. While I haven’t decided exactly what I’m going to tell you, your opinion is interesting about the two points in this survey, thanks in advance for the answers.
And yes, if you have questions, but for some reason you cannot ask them here on Habré - write to email: shocksilien@gmail.com