// 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) } })
<h1>Canvas</h1> <canvas id="graph" width="800" height="500"></canvas> <script type="text/javascript"> var mygraph = new tcg.graph('graph'); </script>
body { margin: 0; padding: 10px; }