Last active
December 12, 2015 04:38
-
-
Save abidibo/4716081 to your computer and use it in GitHub Desktop.
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...
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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