Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
This is a mootools class which permits to draw a temporal graph over a canvas. Features: - grid system (different steps on tx and y axis) - scale factor different for both axis - x axis (time) discrete - y axis continuous - can't draw in the past, always growing x - maybe more...
// new namespace
var tcg = {};
// main graph controller
tcg.graph = new Class({
options: {
x_grid_step: 20, // grid step in px on x axis
y_grid_step: 40, // grid step in px on y axis
x_factor: 1, // multiplier factor of each grid step on x axis (value = m * grid-steps)
y_factor: 10, // multiplier value of each grid step on y axis (value = m * grid-steps)
x0: 40, // x coordinate (from bottom left) in px of the axis origin
y0: 40,// y coordinate (from bottom left) in px of the axis origin
cursor_width: 12, // width of the cursor rectangle
first_point: [0, 0], // first point values [x, y]
},
Implements: [Options],
/*
* There are here three coordinate systems:
* - ccoord: the coordinates of the canvas, starting from top left
* - coord: the coordinates defined by the drawed axis
* - values: the real properties values
*
* So there are some conversion functions.
* The drawed axis coordinates are always used.
*/
initialize: function(canvas_id, options) {
this.setOptions(options);
this.canvas = $(canvas_id);
this.initCanvas();
// temp canvas used for previews
this.initTempCanvas();
// undo redo functionality
this.setUndoRedo();
// init some store useful arrays
this.points = [];
this.redo_canvas = [];
this.undo_canvas = [];
this.redo_points = [];
this.undo_points = [];
// add first point
first_point_coord = this.valuesToCoords(this.options.first_point[0], this.options.first_point[1]);
this.addPoint(first_point_coord.x, first_point_coord.y);
},
/*
* Numeric values to axis coordinates
*/
valuesToCoords: function(vx, vy) {
var x = vx / this.options.x_factor * this.options.x_grid_step;
var y = vy / this.options.y_factor * this.options.y_grid_step;
return {x: x, y: y};
},
coordsToValues: function(x, y) {
var vx = x / this.options.x_grid_step * this.options.x_factor;
var vy = y / this.options.y_grid_step * this.options.y_factor;
return {x: vx, y: vy};
},
/*
* Axis coordinates to canvas coordinates
*/
coordsToCcoords: function(x, y) {
return {x: x + this.options.x0, y: this.y_max - this.options.y0 - y};
},
/*
* Canvas coordinates to axis coordinates
*/
ccoordsToCoords: function(x, y) {
return {x: x - this.options.x0, y: this.y_max - this.options.y0 - y};
},
/*
* Gets canvas dimensions, context and draws the grid
*/
initCanvas: function() {
// get coordinates
this.canvas_coords = this.canvas.getCoordinates(document.body);
this.x_max = this.canvas_coords.width;
this.y_max = this.canvas_coords.height;
// get context
this.ctx = this.canvas.getContext('2d');
// draw grid
this.drawGrid();
},
/*
* Draws the grid
*/
drawGrid: function() {
// x axis grid
this.ctx.beginPath();
['x', 'y'].each(function(axis) {
var dyn = 0;
while(dyn <= this[axis + '_max']) { // @todo check condition
this.drawGridLine(dyn, axis);
dyn += this.options[axis + '_grid_step'];
}
}.bind(this))
},
/*
* Draws a line of the grid
*/
drawGridLine: function(dyn, axis) {
this.ctx.lineWidth = 1;
this.ctx.strokeStyle = '#eee'.hexToRgb();
this.ctx.beginPath();
var x = axis === 'x' ? dyn : 0;
var y = axis === 'y' ? dyn : 0;
var c_coords = this.coordsToCcoords(x, y);
this.ctx.moveTo(c_coords.x, c_coords.y);
var x_f = axis === 'x' ? dyn : this.x_max;
var y_f = axis === 'y' ? dyn : this.y_max;
var cf_coords = this.coordsToCcoords(x_f, y_f);
this.ctx.lineTo(cf_coords.x, cf_coords.y);
this.ctx.closePath();
this.ctx.stroke();
var label = dyn / this.options[axis + '_grid_step'] * this.options[axis + '_factor'];
this.ctx.fillText(label, axis === 'x' ? c_coords.x : c_coords.x - 30, axis === 'x' ? c_coords.y + 30 : c_coords.y);
},
/*
* Creates the temp canvas and add events to it
*/
initTempCanvas: function() {
this.temp_canvas = new Element('canvas.tmp_graph').setProperties({
width: this.x_max,
height: this.y_max
}).setStyles({
position: 'absolute',
left: this.canvas_coords.left + 'px',
top: this.canvas_coords.top + 'px'
}).inject(document.body);
this.temp_ctx = this.temp_canvas.getContext('2d');
this.temp_canvas.addEvent('mousemove', this.dispatchEvent.bind(this));
this.temp_canvas.addEvent('mouseout', this.dispatchEvent.bind(this));
this.temp_canvas.addEvent('click', this.dispatchEvent.bind(this));
},
// Creates the undo, redo buttons and sets their events
setUndoRedo: function() {
this.undo_button = new Element('input', {type: 'button', value: 'undo'}).addEvent('click', function() { this.restoreState('undo') }.bind(this));
this.redo_button = new Element('input', {type: 'button', value: 'redo'}).addEvent('click', function() { this.restoreState('redo') }.bind(this));
var container = new Element('p').adopt(this.undo_button, this.redo_button).inject(this.canvas.getParent());
},
// Adds the canvas coordinates and axis coordinates to the event object before calling the event handler
dispatchEvent: function(evt) {
evt._cx = evt.page.x - this.canvas_coords.left;
evt._cy = evt.page.y - this.canvas_coords.top;
evt_coords = this.ccoordsToCoords(evt._cx, evt._cy);
evt._x = evt_coords.x;
evt._y = evt_coords.y;
this[evt.type](evt);
},
/*
* Preview of the point, link with the last point and values coordinates
*/
mousemove: function(evt) {
// can't go back in time
if(evt._x <= (this.last_point.x + this.options.x_grid_step/2)) {
return null;
}
// grid x coordinate near to mouse pointer (x axis is discrete)
var gx = Math.round(evt._x / this.options.x_grid_step) * this.options.x_grid_step;
// y axis is continuous
var gy = evt._y;
// canvas coordinates
var gcoords = this.coordsToCcoords(gx, gy);
// top left point of the cursor
var nx = gx - this.options.cursor_width/2;
var ny = gy - this.options.cursor_width/2;
// cleae the temp canvas
this.temp_ctx.clearRect(0, 0, this.x_max, this.y_max);
// draw a line between last inserted point and the mouse pointer
this.temp_ctx.beginPath();
var last_point_ccoords = this.coordsToCcoords(this.last_point.x, this.last_point.y);
this.temp_ctx.moveTo(last_point_ccoords.x, last_point_ccoords.y);
this.temp_ctx.lineTo(gcoords.x, gcoords.y);
this.temp_ctx.strokeStyle = '#aaa'.hexToRgb();
this.temp_ctx.stroke();
// show the point values coordinates
var values = this.coordsToValues(gx, gy);
var label = '(' + values.x + ', ' + values.y + ')';
this.temp_ctx.fillText(label, gcoords.x + 10, gcoords.y + 10);
this.temp_ctx.fillStyle = '#aaa'.hexToRgb();
// draw the cursor
this.temp_ctx.fillRect(gcoords.x - this.options.cursor_width/2, gcoords.y - this.options.cursor_width/2, this.options.cursor_width, this.options.cursor_width);
},
/*
* clear preview on mouseout
*/
mouseout: function() {
this.temp_ctx.clearRect(0, 0, this.x_max, this.y_max);
},
/*
* save the point when clicking
*/
click: function(evt) {
// can't draw in the past
if(evt._x <= (this.last_point.x + this.options.x_grid_step/2)) {
return null;
}
// grid coordinates near to mouse pointer (discrete axis)
var gx = Math.round(evt._x / this.options.x_grid_step) * this.options.x_grid_step;
// free move on y axis (continuous axis)
var gy = evt._y;
// add the point to the class array
this.addPoint(gx, gy);
},
addPoint: function(x, y) {
// save the state for history
this.saveHistory('undo');
// draw the point over the real canvas and clear the temp canvas
this.ctx.drawImage(this.temp_canvas, 0, 0);
this.temp_ctx.clearRect(0, 0, this.x_max, this.y_max);
var ccoords = this.coordsToCcoords(x, y);
// I know is such a repetition, but this way the cursor painted over the real canvas has a different color from the one painted on the temp canvas
this.ctx.fillRect(ccoords.x - this.options.cursor_width/2, ccoords.y - this.options.cursor_width/2, this.options.cursor_width, this.options.cursor_width);
// add the point to the storing array
this.points.push({x: x, y: y});
// update the last point (used to draw the line to the mouse pointer)
this.setLastPoint();
},
/*
* I just prefer to have the last point in an instance member
*/
setLastPoint: function() {
this.last_point = this.points[this.points.length - 1];
},
/*
* Saves the state for history
*/
saveHistory: function(dir) {
this[dir + '_canvas'].push(this.canvas.toDataURL("image/png"));
// puff, I want to assign by value!
this[dir + '_points'].push(this.points.slice(0));
},
// Restore a saved state
restoreState: function(dir) {
// no saved state? go away
if(!this[dir + '_canvas'].length) {
return null;
}
// it's necessary to save the present state in the other history direction
this.saveHistory(dir === 'undo' ? 'redo' : 'undo');
// again by value!
this.points = this[dir + '_points'].pop().slice(0);
// get the state to restore from the history array (the last inserted element)
var restore_state = this[dir + '_canvas'].pop();
// draw the state
var img = new Element('img', {'src':restore_state});
img.onload = function() {
this.ctx.clearRect(0, 0, this.x_max, this.y_max);
this.ctx.drawImage(img, 0, 0, this.x_max, this.y_max);
// always update the last point babe!
this.setLastPoint();
}.bind(this)
}
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.