Design, CG Graphics & Inspiration
Creating a Framework For Canvas: Objects and Mouse

Creating a Framework For Canvas: Objects and Mouse

How often do you encounter a question: how can I get whether the mouse is over the element or not, as it is implemented in the framework? In this post, we will help you to answer it and implement such a framework based on Canvas AtomJS.

Global interface

Firstly we should think over the interface of our framework. Let’s name it Canvas Framework, abbreviated – CF. It will be a global variable-a factory for the instance creation. With the first argument we are going to pass a link for the necessary item:

var cf = new CF('#my-canvas');

The realization 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 other objects with the help of this construction:

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

To make it easier all objects will have drag and drop function and will respond to mouse’s movements.

Shapes’ realization

Now we need to define the basic class of the shapes.

// Abstract class of the shapes
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 can see that we need an emptyCanvas – it will be a hidden Canvas, in which we will draw our paths, in order not to break the path of the “main” canvas. Let’s renew the 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 of the following shape will have to implement only 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 to initialize the Mouse. We subscribe to the event mousemove Canvas element, and we will remember the cursor position. The mouse will receive items Shape, will check them, change the hover and cause events mousedown and mouseup. You can see that we are faced with a little cross-browser problem – code layerX / Y is not available in Opera and it is necessary to use offsetX / Y. It is not that critical, but it is important to know about it.

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) {
		// Then we will uselayer*, but further you’d better do it in a more secure way
		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));
	}
});

// then we add mouse to the constructor:
window.CF = atom.Class({
	initialize: function (canvas) {
		[...]
		this.mouse = new Mouse( this.canvas );
	}
});

Now we need to make a canvas update.

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');
	}
});

On the next step we edit our global object so that we could 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);
	}
})

That’s it, let’s 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' ));

Demo

Conclusion

In fact, frameworks are much more than described in this article. For example, the described example can be implemented to the absolutely positioned canvas only; also, there are a lot of optimizations, nuances, etc. It is necessary to adjust the fps for the application, update the canvas only when there are some changes, do not update the status when moving the mouse, but before the drawing, etc. It is a difficult and laborious work. Better to use something readymade, than to implement them from scratch.
By the way, there is an alternative way – use map + area. It has its advantages and disadvantages. It is difficult to synchronize and, most importantly, the inability to implement more complex shapes.

SHARE THIS POST

Pavel is 21 year old web developer from Ukraine. In his spare time he writes articles about the basics of LibCanvas, AtomJS and JavaScript

Subscribe for the hottest posts

Subscribe to our email newsletter for useful tips and freebies.