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