📜 ⬆️ ⬇️

Creating a framework for Canvas: objects and mouse



Among the questions on the Canvas account, questions were most often asked about the internal frameworks - how to understand that the mouse is over the element, as implemented in the frameworks. In the topic, we implement a similar AtomJS Canvas framework.


Global interface


To begin with we will think up the interface of our framework. Let's call it the Canvas Framework, abbreviated CF. This will be a global factory variable for creating the instance.
The first argument we will pass to it is a link to the desired element:
')
var cf = new CF('#my-canvas'); 


The implementation is simple:
 window.CF = atom.Class({ initialize: function (canvas) { this.canvas = atom.dom( canvas ).first; this.ctx = this.canvas.getContext('2d'); } }); 


Then we can create objects using this entity:

 cf.circle([50, 50, 10] , { fill: 'red' , hover : { fill: 'blue' } }); cf.rect ([10, 10, 20, 20], { fill: 'green', hover : { fill: 'blue' } }); 


For simplicity, all the objects we will have to listen to and events of the mouse.

Realization of figures


Now we need to define the base class of the shape.

 //    var Shape = atom.Class({ Implements: [ atom.Class.Events, atom.Class.Options ], cf : null, data : null, hover: false, path: atom.Class.abstractMethod, initialize: function (data, options) { this.data = data; this.setOptions( options ); }, hasPoint: function (x, y) { var ctx = this.cf.emptyCanvas.ctx; this.path( ctx ); return ctx.isPointInPath(x, y); }, draw: function () { var ctx = this.cf.ctx, o = this.options; this.path( ctx ); ctx.save(); ctx.fillStyle = this.hover ? o.hover.fill : o.fill; ctx.fill(); ctx.restore(); } }); 


You see, we will need some emptyCanvas - this will be a hidden Canvas, into which we will draw our paths in order not to disturb the paths of the “combat” canvas. Update CF constructor:

 window.CF = atom.Class({ initialize: function (canvas) { [...] this.emptyCanvas = atom.dom.create( 'canvas', { width: 1, height: 1 }).first; this.emptyCanvas.ctx = this.emptyCanvas.getContext('2d'); } }); 


Each inheriting shape will only have to implement the path method. Let's add a couple of shapes - Rectangle and Circle

 // circle.data == [x, y, radius] var Circle = atom.Class({ Extends: Shape, path: function (ctx) { ctx.beginPath(); ctx.arc( this.data[0], this.data[1], this.data[2], 0, Math.PI * 2, false ); ctx.closePath(); } }); var Rect = atom.Class({ Extends: Shape, path: function (ctx) { ctx.beginPath(); ctx.rect.apply( ctx, this.data ); ctx.closePath(); } }); 


The next thing we need to do is implement the Mouse. We subscribe to the mousemove event of the Canvas element and memorize the position of the cursor. The mouse will receive Shape elements that will check, change their hover and cause them to create mousedown and mouseup events. You can see that we are faced with a light non-crossbrowser - the layerX / Y code is not in Opera and you need to use offsetX / Y there. Not critical, but most importantly, be aware of this)

 var Mouse = atom.Class({ x: 0, y: 0, initialize: function (canvas) { this.elements = []; canvas.bind({ mousemove: this.move.bind(this), mousedown: this.fire.bind(this, 'mousedown'), mouseup: this.fire.bind(this, 'mouseup' ) }); }, add: function (element) { this.elements.push( element ); }, move: function (e) { //   layer*,        if (e.layerX == null) { // opera this.x = e.offsetX; this.y = e.offsetY; } else { // fx, chrome this.x = e.layerX; this.y = e.layerY; } this.elements.forEach(function (el) { el[i].hover = el[i].hasPoint(this.x, this.y) }.bind(this)); }, fire: function (name, e) { this.elements.forEach(function (el) { if (el.hasPoint(this.x, this.y)) { el.fireEvent(name, e); } }.bind(this)); } }); //    : window.CF = atom.Class({ initialize: function (canvas) { [...] this.mouse = new Mouse( this.canvas ); } }); 


Now it is necessary to update the canvas.

 window.CF = atom.Class({ initialize: function (canvas) { [...] // 25 fps this.update.periodical( 1000/25, this ); }, update: function (shapes) { this.ctx.clearRect(0,0,this.canvas.width, this.canvas.height); this.elements.invoke('draw'); } }); 


We edit our global object so that we can create elements:
 window.CF = atom.Class({ [...], elements: [], _shape: function (Class, args) { var e = new Class(args[0], args[1]); this.mouse.add( e ); this.elements.push( e ); e.cf = this; return e; }, circle: function (data, options) { return this._shape(Circle, arguments); }, rect: function (data, options) { return this._shape(Rect, arguments); } }) 


Everything, we create our application:

 var write = function (msg) { atom.dom.create('p').text(msg).appendTo('body'); }; var cf = new CF('canvas'); cf.circle([50, 50, 10] , { fill: 'red' , hover : { fill: 'blue' } }) .addEvent('mousedown', write.bind( window, 'circle mousedown' )); cf.rect ([10, 10, 20, 20], { fill: 'green', hover : { fill: 'blue' } }) .addEvent('mousedown', write.bind( window, 'rect mousedown' )); 


Result



Conclusion


In fact, beyond frameworks is much more than described in this article. For example, the described example will steer only on absolutely positioned canvases, there are also a lot of optimizations, nuances, etc. It is necessary to adjust the fps for the application, update the canvas only with changes, update the statuses not before mouse movements, but before rendering, etc. This is hard and painstaking work. It is better to use something ready than to implement them from scratch.

By the way, there is an alternative way - using map + area . It has advantages, but also disadvantages. These are difficulties in synchronization and, most importantly, the impossibility of implementing more complex shapes .

Fans on the topic of SVG - hold on. There are reasons to use Canvas. Moreover, this topic is not bad as a training. Because I ask at least this time to do without holivar . And you're already tired

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


All Articles