1 /*
  2  * © 2009 ROBO Design
  3  * http://www.robodesign.ro
  4  *
  5  * $Date: 2009-04-21 14:32:11 +0300 $
  6  */
  7 
  8 /**
  9  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
 10  * @fileOverview The paint application core code.
 11  */
 12 
 13 /**
 14  * @class The paint tool application object.
 15  */
 16 function Painter () {
 17   var _self = this;
 18 
 19   /**
 20    * Holds the buffer canvas and context references.
 21    * @type Object
 22    */
 23   this.buffer = {canvas: null, context: null};
 24 
 25   /**
 26    * Holds the current layer ID, canvas and context references.
 27    * @type Object
 28    */
 29   this.layer = {id: null, canvas: null, context: null};
 30 
 31   /**
 32    * The instance of the active tool object.
 33    *
 34    * @type Object
 35    * @see PainterConfig.tool_default holds the ID of the tool which is activated 
 36    * when the application loads.
 37    */
 38   this.tool = null;
 39 
 40   /**
 41    * Holds references to important DOM elements.
 42    *
 43    * @private
 44    * @type Object
 45    */
 46   this.elems = {};
 47 
 48   /**
 49    * Holds the last recorded mouse coordinates and the button state (if it's 
 50    * down or not).
 51    *
 52    * @private
 53    * @type Object
 54    */
 55   this.mouse = {x: 0, y: 0, buttonDown: false};
 56 
 57   /**
 58    * Holds all the removable functionality from the paint application.
 59    *
 60    * @type Object
 61    * @see Painter#actionAdd Add a new action.
 62    * @see Painter#actionRemove Remove an action.
 63    */
 64   this.actions = {};
 65 
 66   /**
 67    * Holds the keyboard event listener object.
 68    *
 69    * @private
 70    * @type lib.dom.KeyboardEventListener
 71    * @see lib.dom.KeyboardEventListener The class dealing with the cross-browser 
 72    * differences in the DOM keyboard events.
 73    */
 74   var kbListener_;
 75 
 76   /**
 77    * Initialize the paint application.
 78    * @private
 79    */
 80   function init () {
 81     if (!window.lang) {
 82       alert('Error: The language object is not available!');
 83       return;
 84     }
 85 
 86     if (!window.PaintTools) {
 87       alert(lang.PaintToolsNotFound);
 88       return;
 89     }
 90 
 91     if (!window.PainterConfig) {
 92       alert(lang.PainterConfigNotFound);
 93       return;
 94     }
 95 
 96     // This application does not yet implement layers support.
 97     // However, there's only little additional work to be done for layers 
 98     // support.
 99     _self.layer.id = 1;
100 
101     // Find the canvas element.
102     _self.layer.canvas = document.getElementById('imageLayer');
103     if (!_self.layer.canvas) {
104       alert(lang.errorCanvasNotFound);
105       return;
106     }
107 
108     if (!_self.layer.canvas.getContext) {
109       alert(lang.errorGetContext);
110       return;
111     }
112 
113     // Get the 2D canvas context.
114     _self.layer.context = _self.layer.canvas.getContext('2d');
115     if (!_self.layer.context) {
116       alert(lang.errorGetContext);
117       return;
118     }
119 
120     // Add the buffer canvas.
121     var container = _self.layer.canvas.parentNode;
122     _self.buffer.canvas = document.createElement('canvas');
123     if (!_self.buffer.canvas) {
124       alert(lang.errorCanvasCreate);
125       return;
126     }
127 
128     _self.buffer.canvas.id     = 'imageBuffer';
129     _self.buffer.canvas.width  = _self.layer.canvas.width;
130     _self.buffer.canvas.height = _self.layer.canvas.height;
131     container.appendChild(_self.buffer.canvas);
132 
133     _self.buffer.context = _self.buffer.canvas.getContext('2d');
134 
135     // Get the tools drop-down.
136     _self.elems.tool_select = document.getElementById('tool');
137     if (!_self.elems.tool_select) {
138       alert(lang.errorToolSelect);
139       return;
140     }
141     _self.elems.tool_select.addEventListener('change', ev_tool_change, false);
142 
143     // Activate the default tool.
144     _self.toolActivate(PainterConfig.tool_default);
145 
146     // Attach the mousedown, mousemove and mouseup event listeners.
147     _self.buffer.canvas.addEventListener('mousedown', ev_canvas, false);
148     _self.buffer.canvas.addEventListener('mousemove', ev_canvas, false);
149     _self.buffer.canvas.addEventListener('mouseup',   ev_canvas, false);
150 
151     // Add the keyboard events handler.
152     kbListener_ = new lib.dom.KeyboardEventListener(window,
153         {keydown: ev_keyhandler, keypress: ev_keyhandler, keyup: ev_keyhandler});
154   };
155 
156   /**
157    * The Canvas event handler.
158    * 
159    * <p>This method determines the mouse position relative to the canvas 
160    * element, after which it invokes the method of the currently active tool 
161    * with the same name as the current event type. For example, for the 
162    * <code>mousedown</code> event the <code><var>tool</var>.mousedown()</code> 
163    * method is invoked.
164    *
165    * <p>The mouse coordinates are added to the <var>ev</var> DOM Event object: 
166    * <var>ev.x_</var> and <var>ev.y_</var>.
167    *
168    * @private
169    *
170    * @param {Event} ev The DOM Event object.
171    *
172    * @returns The result returned by the event handler of the current tool. If 
173    * no event handler was executed, false is returned.
174    */
175   function ev_canvas (ev) {
176     if (!ev.type || !_self.tool) {
177       return false;
178     }
179 
180     /*
181      * If the mouse is down already, skip the event.
182      * This is needed to allow the user to go out of the drawing canvas, release 
183      * the mouse button, then come back and click to end the drawing operation.
184      * Additionally, this is needed to allow extensions like MouseKeys to 
185      * perform their actions during a drawing operation, even when a real mouse 
186      * is used. For example, allow the user to start drawing with the keyboard 
187      * (press 0) then use the real mouse to move and click to end the drawing 
188      * operation.
189      */
190     if (_self.mouse.buttonDown && ev.type == 'mousedown') {
191       return false;
192     }
193 
194     // Don't overwrite any existing x_ / y_ property.
195     // These properties might be added by other functions.
196     if (typeof ev.x_ == 'undefined') {
197       if (typeof ev.layerX != 'undefined') { // Firefox
198         ev.x_ = ev.layerX;
199         ev.y_ = ev.layerY;
200       } else if (typeof ev.offsetX != 'undefined') { // Opera
201         ev.x_ = ev.offsetX;
202         ev.y_ = ev.offsetY;
203       }
204 
205       // Update the current mouse position only for mouse events.
206       // Other events do not provide accurate mouse coordinates.
207       switch (ev.type) {
208         case 'mousedown':
209         case 'mousemove':
210         case 'mouseup':
211           _self.mouse.x = ev.x_;
212           _self.mouse.y = ev.y_;
213       }
214     }
215 
216     if (ev.type == 'mousedown') {
217       _self.mouse.buttonDown = true;
218     }
219 
220     // Call the event handler of the active tool.
221     var func = _self.tool[ev.type];
222     if (typeof func != 'function') {
223       return false;
224     }
225 
226     res = func(ev);
227 
228     if (ev.type == 'mouseup') {
229       _self.mouse.buttonDown = false;
230     }
231 
232     /*
233      * If the event handler from the current tool does return false, it means it 
234      * did not execute for some reason. For example, in a keydown event handler 
235      * the keyboard shortcut does not match some criteria, thus the handler 
236      * returns false, leaving the event continue its normal flow.
237      */
238     if (res !== false && ev.preventDefault) {
239       ev.preventDefault();
240     }
241 
242     return res;
243   };
244 
245   /**
246    * The event handler for any changes made to the tool selector.
247    *
248    * @private
249    * @see Painter#toolActivate The method which does the actual drawing tool 
250    * activation.
251    */
252   function ev_tool_change () {
253     _self.toolActivate(this.value);
254   };
255 
256   /**
257    * Activate a drawing tool by ID.
258    *
259    * <p>The <var>id</var> provided must be available in the global {@link 
260    * PaintTools} object.
261    *
262    * @param {String} id The ID of the drawing tool to be activated.
263    *
264    * @returns {Boolean} True if the tool has been activated, or false if not.
265    *
266    * @see PaintTools The object holding all the drawing tools.
267    */
268   this.toolActivate = function (id) {
269     if (!id) {
270       return false;
271     }
272 
273     // Find the tool object.
274     var tool = PaintTools[id];
275     if (!tool) {
276       return false;
277     }
278 
279     // Check if the current tool is the same as the desired one.
280     if (_self.tool && _self.tool instanceof tool) {
281       return true;
282     }
283 
284     // Construct the new tool object.
285     var tool_obj = new tool(_self);
286     if (!tool_obj) {
287       alert(lang.errorToolActivate);
288       return false;
289     }
290 
291     _self.tool = tool_obj;
292 
293     // Update the tool drop-down.
294     _self.elems.tool_select.value = id;
295 
296     return true;
297   };
298 
299   /**
300    * The global keyboard events handler. This makes all the keyboard shortcuts 
301    * work in the web application.
302    *
303    * <p>This method determines the key the user pressed, based on the 
304    * <var>ev</var> DOM Event object, taking into consideration any browser 
305    * differences. Two new properties are added to the <var>ev</var> object:
306    *
307    * <ul>
308    *   <li><var>ev.kid_</var> is a string holding the key and the modifiers list 
309    *   (<kbd>Control</kbd>, <kbd>Alt</kbd> and/or <kbd>Shift</kbd>). For 
310    *   example, if the user would press the key <kbd>A</kbd> while holding down 
311    *   <kbd>Control</kbd>, then <var>ev.kid_</var> would be "Control A". If the 
312    *   user would press "9" while holding down <kbd>Shift</kbd>, then 
313    *   <var>ev.kid_</var> would be "Shift 9".
314    *
315    *   <li><var>ev.kobj_</var> holds a reference to the keyboard shortcut 
316    *   definition object from the configuration. This is useful for reuse, for 
317    *   passing parameters from the keyboard shortcut configuration object to the 
318    *   event handler.
319    * </ul>
320    *
321    * <p>In {@link PainterConfig.keys} one can setup the keyboard shortcuts. If 
322    * the keyboard combination is found in that list, then the associated tool is 
323    * activated.
324    *
325    * @private
326    *
327    * @param {Event} ev The DOM Event object.
328    *
329    * @see PainterConfig.keys The keyboard shortcuts configuration.
330    * @see lib.dom.KeyboardEventListener The class dealing with the cross-browser 
331    * differences in the DOM keyboard events.
332    */
333   function ev_keyhandler (ev) {
334     if (!ev || !ev.key_) {
335       return;
336     }
337 
338     // Do not continue if the event target is some form input.
339     if (ev.target && ev.target.nodeName) {
340       switch (ev.target.nodeName.toLowerCase()) {
341         case 'input':
342         case 'select':
343           return;
344       }
345     }
346 
347     // Determine the key ID.
348     ev.kid_ = '';
349     var i, kmods = {altKey: 'Alt', ctrlKey: 'Control', shiftKey: 'Shift'};
350     for (i in kmods) {
351       if (ev[i] && ev.key_ != kmods[i]) {
352         ev.kid_ += kmods[i] + ' ';
353       }
354     }
355     ev.kid_ += ev.key_;
356 
357     /*
358      * Send the event to the canvas, and eventually to the keydown event handler 
359      * of the currently active tool (if any).
360      * The effect of calling ev_canvas() is that the event object *might* have 
361      * the x_ and y_ coordinate properties added. Additionally, if ev_canvas() 
362      * returns some result, we can use it to cancel any global keyboard 
363      * shortcut.
364      */
365     var canvas_result = ev_canvas(ev);
366     if (canvas_result) {
367       return;
368     }
369 
370     // If there's no event handler within active tool, or if the event handler 
371     // does otherwise return false, then continue with the global keyboard 
372     // shortcuts.
373 
374     var gkey = PainterConfig.keys[ev.kid_];
375     if (!gkey) {
376       return;
377     }
378 
379     ev.kobj_ = gkey;
380 
381     // Execute the associated action.
382     if (gkey.action) {
383       var action = _self.actions[gkey.action];
384       if (action) {
385         var func = action[ev.type];
386         if (typeof func == 'function') {
387           func(ev);
388         }
389       }
390     }
391 
392     // Activate the tool associated with the current keyboard shortcut.
393     // Do this only once, for the keydown event.
394     if (ev.type == 'keydown' && gkey.tool) {
395       _self.toolActivate(gkey.tool);
396     }
397 
398     if (ev.type == 'keypress' && ev.preventDefault) {
399       ev.preventDefault();
400     }
401   };
402 
403   /**
404    * This method draws the buffer canvas on top of the current image layer, 
405    * after which the buffer is cleared. This function is called each time when 
406    * the user completes a drawing operation.
407    */
408   this.layerUpdate = function () {
409     _self.layer.context.drawImage(_self.buffer.canvas, 0, 0);
410     _self.buffer.context.clearRect(0, 0, _self.buffer.canvas.width, _self.buffer.canvas.height);
411   };
412 
413   /**
414    * Add a new action to the paint tool.
415    *
416    * @param {String} id The ID of the new action.
417    * @param {Function} func The constructor function of the new action object.
418    * @param {Boolean} [overwrite=false] Tells to overwrite or not an existing 
419    * action, with the same ID.
420    *
421    * @returns {Boolean} True if the action was successfully added, or false if 
422    * not.
423    *
424    * @see Painter#actionRemove allows you to remove actions.
425    * @see Painter#actions Holds the action objects.
426    */
427   this.actionAdd = function (id, func, overwrite) {
428     if (typeof id != 'string' || typeof func != 'function' || (this.actions[id] && !overwrite)) {
429       return false;
430     }
431 
432     this.actions[id] = new func(_self);
433 
434     return this.actions[id] ? true : false;
435   };
436 
437 
438   /**
439    * Remove an action from the paint tool.
440    *
441    * <p>If the action object being destructed has the 
442    * <code>actionRemove()</code> method, then it will be invoked, allowing any 
443    * custom action removal code to run.
444    *
445    * @param {String} id The ID of the action object you want to remove.
446    *
447    * @returns {Boolean} True if the action was removed, or false if it does not 
448    * exist or some error occurred.
449    *
450    * @see Painter#actionAdd allows you to add new actions.
451    * @see Painter#actions Holds the action objects.
452    */
453   this.actionRemove = function (id) {
454     if (!id || !_self.actions[id]) {
455       return false;
456     }
457 
458     if (typeof _self.actions[id].actionRemove == 'function') {
459       _self.actions[id].actionRemove();
460     }
461 
462     delete _self.actions[id];
463 
464     return true;
465   };
466 
467   init();
468 };
469 
470 if(window.addEventListener) {
471 window.addEventListener('load', function () {
472   if (window.Painter) {
473     // Create a Painter object instance.
474     window.PainterInstance = new Painter();
475   }
476 }, false); }
477 
478 // vim:set spell spl=en fo=wan1croql tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
479